0

Background

I am working with a collection of opaque types of my own design ("the collection").

At higher levels of my program, I want to pass around handles to each instance of each object having a type in the collection. The lower levels of my program (that know about the details of the type) deal with the underlying structure associated with each type, and apply appropriate operations.

One reason to use this approach—in the case of structs—is (N2176 6.2.5-28):

All pointers to structure types shall have the same representation and alignment requirements as each other.

I want each type to be distinct (there is no inheritance or polymorphism among members of the collection) so that I can take advantage of compile-time detection of type errors. Also, I don't think I understand the difference between "distinct" and not "compatible:" "Two types have compatible type if their types are the same" (ibid. 6.2.7-1).

Headers are similar to

// FILE: value.h
#include <stddef.h>
typedef struct s_myValue myValue;
typedef myValue * myHandle;
typedef const myHandle constMyHandle; // Oops. See comments on typedef.
int value_init(myHandle, size_t);
int value_f1(myHandle, ...);
int value_f2(myHandle, ...);

or

// FILE: value-1.h
#include <stddef.h>
typedef struct s_myValue myValue;
int value_init(myValue *, size_t);
int value_f1(myValue *, ...);
int value_f2(myValue *, ...);
int value_f3(const myValue *, ...);

(Credit goes to another SO user for suggesting these kind of typedefs—can't seem to find a better reference just now.)

In one case, I have decided that at the lower level, I am going to demote each myHandle to a void * for internal processing. Therefore (I think), the only reason to have myValue, myHandle, and constMyHandle is to provide for the interface, and it does not matter how I define struct s_myValue.

Except that all members of the collection must be distinct. How to guarantee this?

Prompted by ibid. (6.7.2.3-5):

Each declaration of a structure, union, or enumerated type which does not include a tag declares a distinct type.

and the specification of sytax for declarations (ibid., A.2.2), I have come up with the following minimal (I think) declaration:

Declaration No. 1 (wrong)

// FILE: value.c  
#include "value.h"
// ...
struct s_myValue
{
    _Static_assert ( 1 , "" ) ;
} ;
// ...

But this seems kludgey. Also, not valid, as pointed out by @kamilcuk (N2176 6.2.5-20):

A structure type describes a sequentially allocated nonempty set of member objects... each of which has an optionally specified name and possibly distinct type.

Declaration No. 2

Maybe:

// FILE: value.c  
#include "value.h"
// ...
struct s_myValue
{
    // tag randomly generated (UUID4, reorganized)
    s_myValue * a6e64fd2eb4689eab294b9524e0efa1 ;
} ;
// ...

But, yuck.

The Question

Is there a more expressive, elegant way to declare a struct that is distinct from all other types? Is there a way to declare that is more consistent with the design of the C programming language? (Yes, I realize that I just used the words "design" and "C" in the same sentence.)

I don't think struct s_myValue { } ; is a candidate, because that declaration seems not to be standard.

Appendix (MRE)

The point is to see how the types impact compilation.

//==> bar.h <==
#ifndef H_BAR
#define H_BAR
typedef struct s_bar bar ;
void bar_init ( bar * ) ;
#endif

//==> bar.c <==
#include "bar.h"
struct s_bar { int i ; } ;
void bar_init ( bar * b ) { ; }

//==> baz.h <==
#ifndef H_BAZ
#define H_BAZ
typedef struct s_baz baz ;
void baz_init ( baz * ) ;
#endif

//==> baz.c <==
#include "baz.h"
struct s_baz { int i ; } ;
void baz_init ( baz * b ) { ; }

//==> foo.h <== (EMPTY FILE)

//==> foo.c <==
#include "bar.h"
#include "baz.h"
#include <stdlib.h>
int main ( void )
{
    bar * pbar = NULL ; // keeping it simple for this example...
    baz * pbaz = NULL ; // ... OK because init-s don't do anything.
    bar_init ( pbar ) ;
    baz_init ( pbar ) ; // passing wrong pointer type intentionally
    return 0 ;
}

//==> Makefile <==
objects=foo.o bar.o baz.o
CC=gcc -std=c17
CCC=$(CC) -c
foo: $(objects)
    $(CC) -o foo $(objects)

foo.o: foo.c foo.h bar.h baz.h 
    $(CCC) foo.c
foo.h: ;
foo.c: ;

bar.o: bar.c bar.h
    $(CCC) bar.c
bar.h: ;
bar.c: ;

baz.o: baz.c baz.h
    $(CCC) baz.c
baz.h: ;
baz.c: ;

GCC produces a warning (not an error):

foo.c: In function ‘main’:
foo.c:9:16: warning: passing argument 1 of ‘baz_init’ from incompatible 
pointer type [-Wincompatible-pointer-types]
    9 |     baz_init ( pbar ) ; // passing wrong pointer type intentionally
      |                ^~~~
      |                |
      |                bar * {aka struct s_bar *}
In file included from foo.c:2:
baz.h:4:17: note: expected ‘baz *’ {aka ‘struct s_baz *’} but argument is of type ‘bar *’ {aka ‘struct s_bar *’}
    4 | void baz_init ( baz * ) ;
      |                 ^~~~~

This is not the type checking I am looking for—maybe I should switch to C++—(ibid. 6.3.2.3-7):

A pointer to an object type may be converted to a pointer to a different object type.

Ana Nimbus
  • 635
  • 3
  • 16
  • Always use a prefix on your symbols to namespace them. This ensures conflict will be your own doing. Drop `myHandle` in favor of adding &* on `myValue`. You can use `__FILE__` and `__LINE__` to automatically create unique names, or even better `__COUNTER__` if your (pre-processor) compiler supports it. – Allan Wind Oct 15 '22 at 21:31
  • 3
    I recommend people who want that level of abstraction not to program in C. – Cheatah Oct 15 '22 at 22:15
  • @AllanWind Re `&*`: Could you post a specific alternative to my `value.h`? – Ana Nimbus Oct 15 '22 at 22:21
  • @Cheatah Re "abstraction:" I actually want _less_ abstraction. Dealing with `void *` and individual bytes is about as concrete as it gets while staying within the standard. I care about the memory layout: I am taking steps toward something that will run on a machine with a 16-bit address space. – Ana Nimbus Oct 15 '22 at 22:30
  • 4
    `typedef myValue * myHandle;` Do not use typedef pointers. They are confusing. `Is there a more expressive, elegant way` Is (or close to) opinion based. Typedef pointers are definitely not elegant. – KamilCuk Oct 15 '22 at 22:37
  • Interesting comments, everyone. Anyone have an answer to the question or a comment that reflects the question? – Ana Nimbus Oct 15 '22 at 22:40
  • What is `typedef const constMyHandle;`? An alias for `const int`? – tstanisl Oct 15 '22 at 22:52
  • @tstanisl Re `constMyHandle`: Oops. Forgot a word. My edit should make that clear. – Ana Nimbus Oct 15 '22 at 22:55
  • I think, you should switch to c++ which has stricter rules for type checking. – Serge Oct 16 '22 at 01:32
  • How do you imagine you would declare a struct that is *not* distinct from all other types? Or maybe that should be, what do you mean by "distinct"? – John Bollinger Oct 16 '22 at 01:59
  • 2
    The standard way to define distinct opaque types is to use incomplete structure declarations. A header used throughout the program simply declares `struct foo; struct bar;` and so on without saying anything about their contents. Pointers to these are distinct types and may be used by code with no knowledge of the structure contents. In a header for parts of the program that need to work with the structure contents, the structures would be fully defined. Are you asking for anything different from thiis? – Eric Postpischil Oct 16 '22 at 03:16
  • 1
    @AnaNimbus, I hope you are aware that `constMyHandle` is an alias for `myValue * const`, not for `const myValue *` – tstanisl Oct 16 '22 at 10:36
  • ... and the doubt about whether you were aware of that reflects some of the many reasons to avoid hiding pointer nature behind typedefs. – John Bollinger Oct 16 '22 at 14:24
  • @tstanisl Re `constMyHandle`: did not realize that. This takes us off topic, though. – Ana Nimbus Oct 18 '22 at 12:58
  • 1
    Hiding pointers behind typedef is universally bad and opaque types is no exception. It's better to define the opaque type as a `typedef struct foo foo;` and then design the API using pointers. Meaning the user has to declare "instances" of the type as pointers too, but it avoids any confusion regarding which type that is actually used. As an example one of the worst design decisions in C coding history is the type system of the Windows API, including it's magical `HANDLE`... which constantly results in users passing around `HANDLE*`... which is actually a pointer-to-pointer, needlessly. – Lundin Oct 18 '22 at 15:07

3 Answers3

1

"Distinct" vs "not compatible"

I take this ...

I want each type to be distinct (there is no inheritance or polymorphism among members of the collection) so that I can take advantage of compile-time detection of type errors.

... as a definition of what you mean by "distinct", but it is not what the language spec means by the same term. With respect to distinct data types, the spec says:

Each unqualified type has several qualified versions of its type, corresponding to the combinations of one, two, or all three of the const, volatile, and restrict qualifiers. The qualified or unqualified versions of a type are distinct types [...]

(C17 6.2.5/26)

and

Two declarations of structure, union, or enumerated types which are in different scopes or use different tags declare distinct types. Each declaration of a structure, union, or enumerated type which does not include a tag declares a distinct type.

(C17 6.7.2.3/5)

The closest the language comes to what you seem to mean is types that are not "compatible". The language's type-matching rules are defined around requirements for compatible type, combined with automatic type conversions under certain circumstances. For example, the specifications for function calls say,

If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.

(C17 6.5.2.2/2)

... and the rules for structure assignment say,

[...] the following shall hold:

[...]

  • the left operand has an atomic, qualified, or unqualified version of a structure or union type compatible with the type of the right

(C17 6.5.16.1/1)

Analogous rules based on compatible types apply to pointer assignment, and therefore to passing pointer arguments to functions.

C has neither type inheritance nor polymorphism as a C++ or Java programmer would recognize them. You don't need to do anything very special to have structure types that are not interoperable from a type-matching perspective.

Meaning and implications of compatible type

Also, I don't think I understand the difference between "distinct" and not "compatible:" "Two types have compatible type if their types are the same" (ibid. 6.2.7-1).

Well yes, if you pluck individual sentences out of their context then you have a good chance of having trouble understanding them. That particular provision is immediately followed by

Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.6 for declarators. Moreover, two structure, union, or enumerated types declared in separate translation units are compatible if [...]

Moreover, although the broader context helps, it is difficult to really understand the language spec other than as an integrated whole. Given that compatible type is a defined term, you really need to consider the definition in full, as well as the significance of having compatible type, such as the details already mentioned above.

In the same vein, this ...

This is not the type checking I am looking for—maybe I should switch to C++—(ibid. 6.3.2.3-7):

A pointer to an object type may be converted to a pointer to a different object type.

... seems to be reading more into the spec than it actually says. That a pointer to one object type can be converted to a different object type does not mean that such conversions are automatic. In fact, C defines automatic conversions between object pointer types only where one of the types is a pointer-to-void type.

Recommendations

Is there a more expressive, elegant way to declare a struct that is distinct from all other types? Is there a way to declare that is more consistent with the design of the C programming language? (Yes, I realize that I just used the words "design" and "C" in the same sentence.)

You seem to be overthinking it. You don't need to do anything much special to declare structure types on which the compiler can perform specific type checking. If you furthermore want to use them as opaque types, then the way to proceed is to declare them with (different) tags:

struct a_tag {
    // one or more members ...
};

struct another_tag {
    // one or more members ...
};

Where you want to refer to these opaquely, you can declare only an incomplete version:

struct a_tag;

struct another_tag;

These are compatible with the previous because the member-matching criteria for structure type compatibility apply only if both types being considered are completed within the translation unit.

You cannot handle a structure directly where its type is incomplete, but you can handle pointers to instances:

struct a_tag;
struct a_tag *create_a(void);
void do_something_with_an_a(struct a_tag *a);

void f(void) {
    struct a_tag *my_a = create_a();
    do_something_with_a(my_a);
}

I don't think struct s_myValue { } ; is a candidate, because that declaration seems not to be standard.

Correct, that is non-standard on account of providing an empty member list (as opposed to no member list at all).

Compiler behavior

You write

GCC produces a warning (not an error):

The language spec does not distinguish between different kinds of diagnostic message, and under no circumstance does it require a conforming C processor to reject a particular code. If you want guarantees of that nature then you are looking for Java (and not C++, either).

For GCC in particular, however, you can specify either in general or on a per-warning-type basis that warnings should be promoted to errors. For example,

gcc -Werror=incompatible-pointer-types

You might want to add -pedantic to that, too.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
0

The following

Each declaration of a structure, union, or enumerated type which does not include a tag declares a distinct type.

says nothing about whether declarations of structure, union, or enumerated types with tags declare unique types.

To paraphrase the standard (specifically C90), I paraphrase because I don't know if I can quote it without violating their copyright.

struct identitifer;

specifies a structure type and declares a tag. It specifies a new type distinct from any type with the same tag.

So all you need is

struct s_myValue {
    //structure member declarations go here.
}
Stuart
  • 1,428
  • 11
  • 20
  • This is so absolutely right and (IMO) obvious that my best guess is that the OP means something different by "distinct". But who knows? – John Bollinger Oct 16 '22 at 02:02
  • 2
    FWIW, it is common practice around here to post quotations of relevant portions of the language specification. With appropriate attribution, of course. I claim that my own instances of such copying are allowed as fair use, and I imagine that others would make the same claim about theirs. – John Bollinger Oct 16 '22 at 02:07
  • @John Bollinger I think you're probably right, after looking at the question again, I realize that my answer doesn't have anything the original question doesn't have, I just took away some stuff that I didn't really understand. But that stuff was intended to do something. – Stuart Oct 16 '22 at 02:58
  • @John_Bollinger thanks for the info about quoting langage specs. My explanation about why I didn't want to do it was misleading (in other words, I was being lazy). I quote lang specs sometimes, but in this case, I felt that if I did I would have to try and explain what the standard was saying, and at the time I thought it was a simple misunderstanding and didn't seem to be worth it (also I might get the explanation wrong). – Stuart Oct 16 '22 at 03:26
  • @JohnBollinger Re "distinct:" I don't understand the difference among the words "distinct," not "compatible," and "unique." I see "distinct" and "compatible" in the standard (N2176). – Ana Nimbus Oct 18 '22 at 14:52
  • 2
    The relevant part of the standard would be C17 6.2.7 regarding compatible types/structs. "If one is declared with a tag, the other shall be declared with the same tag." So the solution is not to cast the struct pointers to `void*` or that uniqueness is lost. Besides, de-referencing a struct as anything but a character pointer or the correct struct type would be UB anyway, so I don't really see what problem a `void*` would solve. – Lundin Oct 18 '22 at 15:01
  • I was confused by "tag" vs. "name," as in "named member." I think I understand "Moreover, two structure... types declared in separate translation units are compatible if their _tags **and** members_ satisfy the following requirements: If one is declared with a tag, the other shall be declared with the same tag" (N2176 6.2.7-1; emphasis added). I presume the converse is true: two structure types are incompatible if they have different tags; two structure types are incompatible if their members are named differently. – Ana Nimbus Oct 18 '22 at 15:04
  • @AnaNimbus More importantly, the compiler will whine if someone tries `struct foo* f; struct bar* b = f;` even if those structs are otherwise identical internally. It's compile-time type safety. Just refrain from using void pointers and that should solve most of your problems. – Lundin Oct 18 '22 at 15:09
  • Do you mean "It specifies a new type distinct from any type" _having a different_ tag?_ I am getting lost at "with the same tag." – Ana Nimbus Oct 18 '22 at 15:14
  • @AnaNimbus, the language spec considers two structure types declared in different translation units to be "distinct" types under all circumstances, regardless of tags, members, typedef aliases, or any other consideration. Under some circumstances, however, they may be *compatible* types. You have been referred to the compatibility requirements, which include, but are not limited to, that either both be declared without any tag or that they be declared with the same tag. – John Bollinger Oct 18 '22 at 18:05
  • 2
    One of the upshots is that defining a structure type in a header gives you *distinct* types in all distinct translation units that `#include` that header, all of them *compatible* with each other. – John Bollinger Oct 18 '22 at 18:11
0

You can use a macro to generate to generate your types and prototypes:

#define value_create(suffix)\
    value_create2(suffix)

#define value_create2(suffix)\
    typedef struct value_##suffix value_##suffix;\
    value_##suffix *value_create_##suffix(value_##suffix *)

value_create(1);
value_create(2);

int main() {
    value_1 *value_create_1();
    value_2 *value_create_2();
}

You would also need to generate a matching implementation:

  1. You can change the macro to also generate the implementation. This means the point of using an opaque pointer goes out the window. See for example CTL, mlib.
  2. You write a different macro that generate the implementation, say, value_create_impl() which you now have to synchronize with use above:
value_create_impl(1);
value_create_impl(2);

To me this would only make sense if there was a difference in the implementation, for instance, you want a container for int and a container for double.

Macros are a blunt and limited tool, and you up with having to change how to call a macro to see if then generate the code you really wanted. My advise is not to down this route, or at least look at existing solutions first to see if you like it.

Allan Wind
  • 23,068
  • 5
  • 28
  • 38