Proposal for C2y/C3x

Abstract

Generic structures usually need compile-time data or functions associated with the type. This paper explores a mechanism for structs to contain constexpr members.

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, strs_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 strs_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 strs_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 constexpr struct members, such members don’t contribute to the size or alignment of the struct. 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);
}

constexpr struct members are not regular members. They can’t be bit-fields, nor be anonymous.

In order for two structs to be compatible, on top of the existing rules, their constexpr members must be equal. If two structs are compatible, their constexpr members may be different objects:

Example 5

struct S
{
    constexpr int x = 1;
    int y;
};

const int *p1 = &(struct S).x;

void foo()
{
    struct S
    {
        constexpr int x = 1;
        int y;
    };

    const int *p2 = &(struct S).x;
    assert((p1 == p2) || (p1 != p2));
}

constexpr member equality

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)

If two structs are being checked for compatibility (such as struct S in Example 5), constexpr members must be equal.

Three ways to tackle it:

Option A:

Each constexpr member is compared:

Option B:

Each constexpr member is compared byte-wise. This approach may not work because of padding bytes. For it to work, the standard needs to require constexpr structs and unions to have their padding bytes always be zeros. It also may not work as expected for floating types (e.g. +0.0 and -0.0).

Option C:

Only types that can be checked are numeric types and pointers, and they are compared as if by ==, the member is unequal otherwise. This approach is the simplest, but reduces the usability of this feature (no interface structs).


This paper proposes Option C for now, it’s a good starting point and can be expanded on with a later proposal.

Example 7: 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;

Alternative Approach

If WG14 would rather this be more similar to C++, then constexpr members are static constexpr instead of just 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)::i - typeof(struct S)::i

The lhs must be a single identifier. In my opinion this makes this approach a no-go, many projects don’t 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 (implementing Option A for constexpr member equality), but it’s on top of the current C23 rules. So no constexpr function pointers nor tagless struct compatibility. The biggest difference is I compare each union member, not only the initialized member (this was done for ease of implementation).

References