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.
"Distinct" vs "not compatible"
I take this ...
... 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:
(C17 6.2.5/26)
and
(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,
(C17 6.5.2.2/2)
... and the rules for structure assignment say,
(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
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
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 ...
... 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-
voidtype.Recommendations
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:
Where you want to refer to these opaquely, you can declare only an incomplete version:
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:
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
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,
You might want to add
-pedanticto that, too.