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 struct
s—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 typedef
s—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:
// 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.
Maybe:
// FILE: value.c
#include "value.h"
// ...
struct s_myValue
{
// tag randomly generated (UUID4, reorganized)
s_myValue * a6e64fd2eb4689eab294b9524e0efa1 ;
} ;
// ...
But, yuck.
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.
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.
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
, andrestrict
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.
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.
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).
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.