Proposal for C2y/C3x

Changes

Abstract

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.

Dependency

For this feature to be much more useful, it depends on these proposals to be accepted first:

Rationale

When designing a generic structure in C using macros, data and functions that are known at compile-time still have to be normal members in the struct.

Example 1

#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.

Example 2

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).

Example 3

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);
}

Proposal

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.

Example 4

struct S
{
    constexpr int i = 10;
    int x;
};

int main()
{
    printf("%d\n", (struct S).i);
}

The reason for using different syntax than normal member access is to avoid mistakingly using them as if their storage was in the containing struct (e.g. offsetof, container_of, etc.).

Type associated constants are not members. They can’t be bit-fields, nor be anonymous.

Example 5: Example 1 updated to use this feature

#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, in addition to 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.

Example 6

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)

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.

For anonymous structs/unions, the type associated constant belongs to the containing struct/union.

Example 7

struct X
{
    struct
    {
        constexpr int i = 1;
        int j;
    };
    int k;
};

static_assert((struct X).i == 1);

Type associated constants can be used when inside its containing struct/union:

Example 8

struct A
{
    constexpr int i = 2;
    constexpr int j = (struct A).i * 2;
    int k;
};

Alternative Approach

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:

The 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.

Another concern with this syntax is that it would be the first instance in C where an expression can start with a type name. Many compilers have a function that checks whether the next token represents a type or not, and allowing type-name :: identifier may make parsing a bit more difficult.

For example, this is how gcc checks for an enum with an underlying type:

if (c_parser_next_token_is (parser, CPP_COLON)
      /* Distinguish an enum-type-specifier from a bit-field
     declaration of the form "enum e : constant-expression;".  */
      && c_token_starts_typename (c_parser_peek_2nd_token (parser)))

If type-name :: identifier was a valid expression in C, then a compiler would need more than one token to determine if its currently parsing a type name or not.

Example 9

_Generic(0,
    enum E  : T::x,
    default : 0
);

The tokens enum E : T match an enum specifier with an underlying type, but T::x is actually the generic association expression.

I think it’s simpler overall to continue to be able to distinguish types and expressions by their first token.

Prior Art

Similar to C++’s static constexpr struct members, with the added complication that C (as of C23) allows redefining the same struct.

Proposed Wording

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 as specified in 6.7.3.2.

Edit paragraph 4

If the declaration of a file scope identifier for:

then the identifier has internal linkage.

Edit paragraph 8

The following identifiers have no linkage: an identifier declared to be anything other than an object or a function; an identifier declared to be a function parameter; a block scope identifier for an object declared without the storage-class specifier extern; the identifier of 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.4

Edit paragraph 3

An object whose identifier is declared without the storage-class specifier thread_local, and either with external or internal linkage or with the storage-class specifier static, or that is a type associated constant, has static storage duration. Its lifetime is the entire execution of the program and its stored value is initialized only once, prior to program startup.

Edit paragraph 5

An object that is not a type associated constant and whose identifier is declared with no linkage and without the storage-class specifier static has automatic storage duration, as do some compound literals. The result of attempting to indirectly access an object with automatic storage duration from a thread other than the one with which the object is associated is implementation-defined.

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, and either have non-floating scalar types and compare equal, or have floating types and have the same object representation ignoring padding bits. 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 parenthesized 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.1

Constraints

Edit paragraph 4

If an identifier has no linkage, there shall be no more than one declaration of the identifier (in a declarator or type specifier) with the same scope and in the same name space, except that:

Semantics

Edit paragraph 13

A declaration such that the declaration specifiers contain no type specifier or that is declared with constexpr is said to be underspecified. If such a declaration is not a definition, if it declares no or more than one ordinary identifier, if it has no declarator, if it has more than one declarator, if the declared identifier already has a declaration in the same scope and neither is a type associated constant, if the declared entity is not an object, or if anywhere within the sequence of tokens making up the declaration identifiers that are not ordinary other than the declarator’s identifier, are declared, the behavior is implementation-defined.

6.7.2

Edit paragraph 18

NOTE 2 An object that is not a type associated constant declared in block scope with a storage-class specifier constexpr and without static has automatic storage duration, the identifier has no linkage, and each instance of the object has a unique address obtainable with & (if it is not declared with the register specifier), if any. Such an object in file scope has static storage duration, the corresponding identifier has internal linkage, and each translation unit that sees the same textual definition implements a separate object with a distinct address.

6.7.3.2

Edit member-declaration-list:

member-declaration-list:
    member-declaration
    member-declaration-list member-declaration
    member-declaration-list-item
    member-declaration-list member-declaration-list-item

member-declaration-list-item:
    member-declaration
    type-associated-constant-declaration

type-associated-constant-declaration:
    declaration

Add a paragraph to Constraints:

A type associated constant declaration shall be a single object declaration of the form

attribute-specifier-sequence-opt declaration-specifiers declarator = initializer ;

and its declaration specifiers shall contain the constexpr storage-class specifier, without any other storage-class specifier.

Semantics

Edit paragraph 16

An unnamed member whose type specifier is a structure specifier with no tag is called an anonymous structure; an unnamed member whose type specifier is a union specifier with no tag is called an anonymous union. The members of an anonymous structure or union are members of the containing structure or union, keeping their structure or union layout., and the type associated constants of an anonymous structure or union are type associated constants of the containing structure or union. This applies recursively if the containing structure or union is also anonymous.

Add a paragraph

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. A type associated constant declaration is a constexpr object declaration, the semantics of which are further explained in 6.7.2. A type associated constant denotes a unique entity, unless another type associated constant with the same name is declared in the specifier of a compatible struct or union in the same translation unit, in which case it is unspecified whether they denote the same entity.

References