Generic structures usually need compile-time data or functions associated with the type. This paper proposes type associated constants, which associate constexpr data with a structure or union.
For this feature to be much more useful, it depends on these proposals to be accepted first:
When designing a macro generic structure in C, data and functions that are known at compile-time still have to be normal members in the struct.
#define hashmap(K, V) \
struct \
{ \
unsigned (*hash)(K); \
bool (*eq)(K, K); \
\
typeof(K) *keys; \
typeof(V) *vals; \
unsigned len, cap; \
}
#define hm_eq_t(hm) typeof((hm)->eq)
#define hm_hash_t(hm) typeof((hm)->hash)
#define hm_key_t(hm) typeof((hm)->keys[0])
#define hm_val_t(hm) typeof((hm)->vals[0])
#define hm_init(hm_, hash_, eq_) \
(static void(typeof(hm_) hm, hm_hash_t(hm) hash, hm_eq_t(hm) eq)) \
{ \
hm->hash = hash; \
hm->eq = eq; \
}(hm_, hash_, eq_)
#define hm_put(hm_, key_, val_) \
(static hm_val_t(hm_) *(typeof(hm_) hm, hm_key_t(hm) key, hm_val_t(hm) val)) \
{ \
/*impl*/ \
}(hm_, key_, val_)
// usage:
int main()
{
hashmap(char *, int) hm;
hm_init(&hm, str_hash, str_eq);
hm_put(&hm, "", 1);
}In the above example, the hashmap struct contains function pointers (eq, hash) that are knowable at compile-time. This is less performant because inlining becomes much more unlikely, and the size of the struct is larger. C++ has member functions, so they don’t need to store function pointers.
template<typename K, typename V, uint32_t HASH(K), bool EQ(K, K)>
struct hashmap
{
uint32_t invoke_hash(K key)
{
return HASH(key);
}
bool invoke_eq(K a, K b)
{
return EQ(a, b);
}
// etc.
};There are C libraries that do header template instantiation to achieve a similar effect, but they are not as convenient to use (e.g. STC)
bool str_eq(char *const *, char *const *);
unsigned str_hash(char *const *);
#define T str_int_map, char*, int
#define i_hash str_hash
#define i_eq str_eq
#include "stc/hashmap.h"
int main()
{
str_int_map map = str_int_map_init();
str_int_map_put(&map, "a", 1);
}This paper proposes adding “type associated constants”, they are
constexpr object declarations that appear inside a member declaration
list of a struct or union. type associated constants don’t contribute to
the size or alignment of the struct or union. They are similar to C++’s
static constexpr members.
To access them, the syntax used is
(type-name).identifier to avoid confusion with normal
members.
struct S
{
constexpr int i = 10;
int x;
};
int main()
{
printf("%d\n", (struct S).i);
}type associated constants are not members. They can’t be bit-fields, nor be anonymous.
#define hashmap(K, V, HASH, EQ) \
struct \
{ \
constexpr unsigned (*hash)(K) = HASH; \
constexpr bool (*eq)(K, K) = EQ; \
\
typeof(K) *keys; \
typeof(V) *vals; \
unsigned len, cap; \
}
hashmap(char*, int, str_hash, str_eq) hm;In order for two structs or unions within the same TU to be
compatible, on top of the existing rules, their type associated
constants must have scalar types and be equal. The reason for requiring
this is type safety. For example, if a hashmap was declared with
hash1, assigning that hashmap to another one that has
hash2 could break things.
hashmap(char*, int, str_hash, str_eq) hm1;
// use hm1 ...
hashmap(char*, int, str_hash2, str_eq) hm2 = hm1;
// constraint violation: hm1 and hm2 have incompatible types
// (if this were allowed, using hm2 would cause unexpected results)And the reason for not requiring cross TU compatibility to depend on
type associated constants is because of usage of static
functions (e.g. if str_hash was static, a
function pointer to it wouldn’t compare equal across TUs).
The only types that can be checked for equality are scalar types, the structs/unions are not compatible otherwise. This approach is simple, but reduces the usability of this feature (no interface structs). A future proposal can expand on this.
If WG14 would rather this be more similar to C++, then instead of
constexpr, the declarations would be
static constexpr. And the syntax to access them would be
identifier::identifier. One problem with this approach is
that these don’t work:
struct S::i(struct S)::itypeof(struct S)::iThe first operand must be a single identifier. In my opinion this makes this approach a no-go, many projects don’t even typedef their structs.
Similar to C++’s static constexpr struct members, with
the added complication that C (as of C23) allows re-defining the same
struct.
I partially implemented this feature as a proof-of-concept in my fork of slimcc but it’s on top of the current C23 rules. So no constexpr function pointers nor tagless struct compatibility.
Based on n3886.
6.2.2
Edit paragraph 3
… Each declaration of an identifier with no linkage denotes a unique entity , unless it is a type associated constant.
6.2.3
Edit the third bullet point:
the members and type associated constants of structures or unions; each structure or union has a separate name space for its members (disambiguated by the type of the expression used to access the member via the . or -> operator) and its type associated constants (disambiguated by the type name used to access the type associated constant via the . operator);
6.2.7
Edit paragraph 1
…
For two structures, corresponding members shall be declared in the same
order. For two structures or unions declared in the
same translation unit, there shall be a one-to-one correspondence
between their type associated constants such that each pair of
corresponding type associated constants are declared with the same name,
have compatible types, have scalar types, and compare equal. For
two unions declared in the same translation unit, corresponding members
shall be declared in the same order.
6.5.3.1
Add a production to
postfix-expression:
postfix-expression:
primary-expression
postfix-expression [ expression ]
postfix-expression ( argument-expression-list-opt )
postfix-expression . identifier
postfix-expression -> identifier
postfix-expression ++
postfix-expression --
compound-literal
(type-name) . identifier
6.5.3.4
Edit paragraph 1 of Constraints:
The operands of the . operator shall be either:
Add paragraph to Semantics:
A parenthesised type name followed by the . operator and an identifier designates a type associated constant of that structure or union type. The value is that of the named type associated constant, and is an lvalue.
6.6.1
Description
Edit paragraph 8
…is a named constant, as is a postfix expression that applies the . member access operator to a named constant of structure or union type, even recursively, or a postfix expression that applies the . type associated constant access operator. For enumeration and predefined constants, their value and type are defined in the respective clauses; for constexpr objects, such a named constant is a constant expression with the type and value of the declared object.
6.7.3.2
Add a production tomember-declaration:
member-declaration:
attribute-specifier-sequence-opt specifier-qualifier-list member-declarator-list-opt ;
static_assert-declaration
type-associated-constant-declaration
type-associated-constant-declaration:
attribute-specifier-sequence-opt declaration-specifiers init-declarator ;
Add a paragraph to Constraints:
The declaration-specifiers of the
type-associated-constant-declaration shall contain the
constexpr storage-class specifier, without any other storage-class
specifier.
Add a paragraph to Semantics:
A
type-associated-constant-declaration does not declare a
member, but a type associated constant of the structure or
union it is being declared in. A type associated constant is an object
with static storage duration and no linkage, and is in the name space of
the structure or union it is being declared in. Type associated
constants of the same structure or union type that have the same name
denote the same object.