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 macro generic structure in C, 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 to avoid confusion with normal members.

Example 4

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.

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

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)

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.

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.

Prior Art

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.

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.

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 to member-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.

References