type-safe parametric polymorphism


Among the many different new proposals for C23 that WG14 obtained are two sensible gems by Alex Gilding

The primary might be what you’d count on if you understand the C++ characteristic and the way it began (I believe in C++11), particularly a risk to specify constants for any sort (if used to declare an object) or to have easy perform calls that may be compelled to be evaluated at compile time. There are nonetheless some tough edges to chop (storage class, linkage) however principally, if Wg14 weren’t so predictably unpredictable, this ought to be a homerun.

The opposite one is on an entire totally different degree, eye opening and really, very promissing. Its major concept is so as to add annotations to APIs to keep away from breaking ABIs and code duplication on the similar time.

Let’s take a primary instance what the options which might be proposed can do. For the rest of this submit I can be assuming that we’ve a C23 compiler that implements the options.

void * memcpy(void * prohibit s1, const void *prohibit s2, size_t n);

This perform makes use of the catch-all sort void to point that the pointers that it receives as arguments might level to any bytes, so long as for each there are no less than n bytes accessible, the 2 byte arrays don’t overlap, and so long as the thing pointed-to by s1 is mutable.

Now in 99% of the circumstances the sorts of the objects which might be pointed-to are most likely the identical and the byte values which might be copied from s2 into s1 will mix to a wise object within the goal. Issues happen if the pointed to things have totally different dimension or in the event that they even have totally different sort. Even when float has 32 bit the next code is inaccurate, as a result of it overwrites an object with floating level sort by a illustration of an integer sort, and the ensuing illustration won’t be legitimate.

uint32_t a = 4288912;
float x;
memcpy(&x, &a, sizeof(x)); // semantic error, do not do that!
printf("an unsigned as a float: %g", x); // might entice

The treatment utilizing options from the above papers seems to be as follows

constexpr void [[bind_var(A)]]* (*cpy_typed)(void [[bind_var(A)]]* prohibit s1, const void [[bind_var(A)]]*prohibit s2, size_t n) = memcpy;
...
uint32_t a;
float x = 45.9;
cpy_typed(&a, &x, sizeof(a)); // Contraint violation, prognosis required!

So what had been undefined habits above, now turns into a tough error (constraint violation as we name it in C), however the perform that’s used beneath, memcpy, continues to be precisely the identical.

The trick is to annotate the void* pointers of the orginal interface with attributes a brand new C23 characteristic (borrowed from C++) that enables to annotate precice spots of the syntax. Right here it’s 3 times the identical attribute [[bind_var(A)]], indicating that the code that makes use of this interface is predicted to go in two pointers which have the identical base sort named A (which ever that’s) and likewise to return a pointer to an object of that very same base sort A.

Now all this specification is placed on a perform pointer cpy_typed that’s constexpr and initialized to memcpy (scroll to the tip of the road to see this.)

No additional code is generated, however sort security is improved!

In C23 with type-generic lambdas, if that’s accepted, you might do one thing like this

#outline cpy_typed(S1, S2, N)                            
[](auto* s1, typeof(*s1) const* s2, size_t n) {         
  return (typeof(*s1)*)memcpy(s1, s2, n); }(S1, S2, N); 
}(S1, S2, N)

Utilizing such a lambda would supply the identical sort checks, however on the potential value of effectivity and code dimension. We might be relying on the optimization capacities of the compiler, the decision to memcpy would typically have an extra indirection and changing the lambda to a perform pointer might indicate the technology of an extra stub every time.

The brand new characteristic turns into much more fascinating after we look into capabilities that obtain a callback to carry out a activity that depends upon such a perform. Once more, let’s have a look at a perform of the C library

void qsort(void *base, size_t nmemb, size_t dimension, int (*compar)(const void *, const void *));

int comp_double(void const* x, void const* y) {
    double const* X = x; double const* Y;
    return (*X < *Y) ? -1 : (*X == *Y ? 0 : +1);
}
...
double   dArr[5] = { 4.0, 2.2, 5.1, 6.3, 9.4, };
unsigned uArr[5] = { 4, 2, 5, 6, 9, };

qsort(dArr, 5, sizeof dArr[0], comp_double); // okay
qsort(uArr, 5, sizeof uArr[0], comp_double); // undefined

If you’re not used to this type of syntax, it is a perform that types the array to which base factors. That array is meant to be composed of nmemb components of dimension dimension and compar is a pointer to a comparability perform that receives two pointers, compares the objects and returns values much less, equal or larger then 0 if the primary is smaller, equal or larger. The underlying sort for base and the kind order that compar describes might be arbitrary.

Now, once more C simply places the duty that each one of that is constant in your (the programmers) shoulders. For now there is no such thing as a mechanism in C that would present even the best consistency checks. However we acquire one perform, one single exterior image within the C library that’s able to doing a generic type. Often it’s comparatively environment friendly and small.

How this modifications with the proposed papers? First this could possibly be augmented in an analogous approach by including attributes

constexpr void (*sort_typed)(void [[bind_var(A)]] *base, size_t nmemb, size_t dimension, int (*compar)(const void [[bind_var(A)]]*, const void [[bind_var(A)]]*)) = qsort;

So now it is a perform pointer that factors to the qsort perform, however that ensures that it might solely be known as with constant sorts.

int comp_double(void const [[bind_type(double)]]* x, void const [[bind_type(double)]]* y) {
    double const* X = x; double const* Y;
    return (*X < *Y) ? -1 : (*X == *Y ? 0 : +1);
}
...
double   dArr[5] = { 4.0, 2.2, 5.1, 6.3, 9.4, };
unsigned uArr[5] = { 4, 2, 5, 6, 9, };

sort_typed(dArr, 5, sizeof dArr[0], comp_double); // okay
sort_typed(uArr, 5, sizeof uArr[0], comp_double); // laborious error

So now the compiler is aware of {that a} perform is predicted that receives pointer the identical pointer goal sort than dArr and uArr. For dArr this base sort is double so every little thing works out tremendous. For uArr it will expect unsigned and so it is ready to alert the programmer that the particular name is inconsistent.

Right here we’ve the second attribute syntax [[bind_type(double)]] that’s launched by the paper. It ensures that comp_double can solely be known as with pointers coming from double. For the perform definition it additionally ensures that the parameters can solely be silently transformed to tips that could double and nothing else.

So as an alternative of blowing up the C library by an unbounded variety of capabilities (as for instance templates in C++ or type-specific capabilities and type-generic interfaces to them in C) Alex’ trick helps us to take care of precisely the one perform that has been within the C library because the epoch (no ABI change), and to enhance the capability of the compiler to detect inconsistencies (a change within the API).

Leave a Reply

Your email address will not be published.