Added garfield and snoopy classes to ./example which are derived from the cat and dog classes.
This commit is contained in:
parent
6ca258f882
commit
32e7d0b337
13 changed files with 1519 additions and 69 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
build/
|
build/
|
||||||
|
ideas/
|
||||||
pkg/
|
pkg/
|
||||||
|
|
||||||
|
|
|
||||||
9
Makefile
9
Makefile
|
|
@ -39,10 +39,13 @@ FORCE:
|
||||||
$(PKGCONFIG): libooc.pc.in FORCE | $(BUILD_DIR); sed -e 's|@prefix@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' $< > $@
|
$(PKGCONFIG): libooc.pc.in FORCE | $(BUILD_DIR); sed -e 's|@prefix@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' $< > $@
|
||||||
|
|
||||||
example: shared
|
example: shared
|
||||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iinclude -Iexample example/main.c example/animal.c example/dog.c example/cat.c -L$(BUILD_DIR) -Wl,-rpath,'$$ORIGIN' -looc -o $(BUILD_DIR)/example
|
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iinclude -Iexample example/main.c example/animal.c example/dog.c example/cat.c example/garfield.c example/snoopy.c -L$(BUILD_DIR) -Wl,-rpath,'$$ORIGIN' -looc -o $(BUILD_DIR)/example
|
||||||
|
|
||||||
$(BUILD_DIR)/safety-test: tests/safety.c example/animal.c example/dog.c example/cat.c example/animal.h example/dog.h example/cat.h $(LIB_STATIC)
|
EXAMPLE_SRCS=example/animal.c example/dog.c example/cat.c example/garfield.c example/snoopy.c
|
||||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iexample tests/safety.c example/animal.c example/dog.c example/cat.c $(LIB_STATIC) -o $@
|
EXAMPLE_HDRS=example/animal.h example/dog.h example/cat.h example/garfield.h example/snoopy.h
|
||||||
|
|
||||||
|
$(BUILD_DIR)/safety-test: tests/safety.c $(EXAMPLE_SRCS) $(EXAMPLE_HDRS) $(LIB_STATIC)
|
||||||
|
$(CC) $(CPPFLAGS) $(CFLAGS) $(WARNINGS) $(LDFLAGS) -Iexample tests/safety.c $(EXAMPLE_SRCS) $(LIB_STATIC) -o $@
|
||||||
|
|
||||||
example-test: example
|
example-test: example
|
||||||
LD_LIBRARY_PATH=$(BUILD_DIR) $(BUILD_DIR)/example
|
LD_LIBRARY_PATH=$(BUILD_DIR) $(BUILD_DIR)/example
|
||||||
|
|
|
||||||
96
README.md
96
README.md
|
|
@ -45,17 +45,69 @@ If installed under `/usr/local` and your pkg-config does not search there:
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The `example/` directory contains an `Animal` base class and two derived classes, `Dog` and `Cat`. Both override the virtual `speak` method, and `example/main.c` walks each of them through the same calls, so the output shows one call site reaching two different implementations. Objects are reference-counted with `ooc_retain()` and `ooc_release()`.
|
The `example/` directory holds a small class hierarchy. `Animal` is the root,
|
||||||
|
`Dog` and `Cat` derive from it, and `Garfield` and `Snoopy` derive from those:
|
||||||
|
|
||||||
|
Animal
|
||||||
|
|-- Dog
|
||||||
|
| `-- Snoopy
|
||||||
|
`-- Cat
|
||||||
|
`-- Garfield
|
||||||
|
|
||||||
|
Every class overrides the virtual `speak` method, and `example/main.c` walks each
|
||||||
|
one through the same calls before putting all four objects through a single
|
||||||
|
`Animal *`. The output makes the point that is invisible in the source: one call
|
||||||
|
site reaches four implementations, and the vtable decides which.
|
||||||
|
|
||||||
make test # runs example/main.c
|
make test # runs example/main.c
|
||||||
make safety-test # runs the assertion suite in tests/
|
make safety-test # runs the assertion suite in tests/
|
||||||
|
|
||||||
|
Objects are reference-counted with `ooc_retain()` and `ooc_release()`.
|
||||||
|
|
||||||
The public installed header is:
|
The public installed header is:
|
||||||
|
|
||||||
#include <ooc/ooc.h>
|
#include <ooc/ooc.h>
|
||||||
|
|
||||||
Application-specific classes such as `Animal`, `Dog` and `Cat` are not installed by the library.
|
Application-specific classes such as `Animal`, `Dog` and `Cat` are not installed by the library.
|
||||||
|
|
||||||
|
### Constructors down a chain
|
||||||
|
|
||||||
|
Each class publishes an `init` function alongside its constructor, so a subclass
|
||||||
|
builds the classes above it by calling them rather than repeating what they do:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int garfield_init(Garfield *garfield, const char *name, int age,
|
||||||
|
const char *colour, const char *favourite_food);
|
||||||
|
```
|
||||||
|
|
||||||
|
`garfield_init()` calls `cat_init()`, which calls `animal_init()`. Each one
|
||||||
|
installs its own vtable, and the one above overwrites it, so the object ends up
|
||||||
|
speaking as the most derived class. A subclass of `Garfield` would call
|
||||||
|
`garfield_init()` in turn, and the composition extends without anything else
|
||||||
|
changing.
|
||||||
|
|
||||||
|
### Destructors down a chain
|
||||||
|
|
||||||
|
The library calls the destructor belonging to the object's runtime type and stops
|
||||||
|
there. A derived destructor is therefore responsible for the whole hierarchy, not
|
||||||
|
only for its own members, and the frees grow with the depth:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void destroy(ooc_object *object)
|
||||||
|
{
|
||||||
|
Garfield *garfield = (Garfield *)object;
|
||||||
|
|
||||||
|
free(garfield->favourite_food); /* Garfield's */
|
||||||
|
free(garfield->cat.colour); /* Cat's, allocated by cat_init() */
|
||||||
|
free(garfield->cat.animal.name); /* Animal's, allocated by animal_init() */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cat's own destructor never runs for a Garfield, and Animal's never runs for
|
||||||
|
either. Omitting a middle free is silent -- the object works perfectly well and
|
||||||
|
only a memory checker reports the leak, which is why `make safety-test` is worth
|
||||||
|
running under Valgrind or AddressSanitizer.
|
||||||
|
|
||||||
## Fields by name
|
## Fields by name
|
||||||
|
|
||||||
A class publishes the fields that may be reached dynamically as a NULL-terminated
|
A class publishes the fields that may be reached dynamically as a NULL-terminated
|
||||||
|
|
@ -91,6 +143,17 @@ chain: a `Dog` resolves `name` and `age` through `Animal_class`, and a field
|
||||||
declared by a subclass shadows a same-named field of its base. Upcasting a
|
declared by a subclass shadows a same-named field of its base. Upcasting a
|
||||||
pointer does not change this -- the runtime type of the object decides.
|
pointer does not change this -- the runtime type of the object decides.
|
||||||
|
|
||||||
|
Lookup walks as far as the chain goes, and a class inserted in the middle hides
|
||||||
|
nothing. A `Garfield` resolves its own `favourite_food`, Cat's `colour` and
|
||||||
|
Animal's `name`, while `imagination` -- declared on the other branch -- resolves
|
||||||
|
to nothing at all:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ooc_get(garfield, "colour"); /* Cat's */
|
||||||
|
ooc_get(garfield, "name"); /* Animal's, two records up */
|
||||||
|
ooc_get(garfield, "imagination"); /* NULL -- Snoopy's field, not in this chain */
|
||||||
|
```
|
||||||
|
|
||||||
`ooc_set()` copies the field's storage with overlap-safe semantics and returns `-1` for an
|
`ooc_set()` copies the field's storage with overlap-safe semantics and returns `-1` for an
|
||||||
unknown field name or a NULL argument, so a bad name is reported instead of
|
unknown field name or a NULL argument, so a bad name is reported instead of
|
||||||
writing out of bounds.
|
writing out of bounds.
|
||||||
|
|
@ -114,6 +177,11 @@ not be shared with a second field, or the same allocation would be freed twice.
|
||||||
Assigning a field the pointer it already holds frees nothing. The destructor
|
Assigning a field the pointer it already holds frees nothing. The destructor
|
||||||
still frees whatever the field holds when the object is released.
|
still frees whatever the field holds when the object is released.
|
||||||
|
|
||||||
|
The rule is the library's rather than the class', so it applies to an inherited
|
||||||
|
field exactly as to one declared alongside it. Setting a Garfield's inherited
|
||||||
|
`colour` releases the string Cat's constructor allocated, the same as setting its
|
||||||
|
own `favourite_food` does.
|
||||||
|
|
||||||
## Type checks
|
## Type checks
|
||||||
|
|
||||||
`ooc_is_a()` walks the inheritance chain from the object's runtime type, so it
|
`ooc_is_a()` walks the inheritance chain from the object's runtime type, so it
|
||||||
|
|
@ -125,6 +193,15 @@ ooc_is_a(cat, &Animal_class); /* 1 -- a Cat is an Animal */
|
||||||
ooc_is_a(cat, &Dog_class); /* 0 -- siblings are unrelated */
|
ooc_is_a(cat, &Dog_class); /* 0 -- siblings are unrelated */
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The walk covers the whole chain, so depth costs nothing to the caller:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ooc_is_a(garfield, &Garfield_class); /* 1 */
|
||||||
|
ooc_is_a(garfield, &Cat_class); /* 1 -- Garfield -> Cat -> Animal */
|
||||||
|
ooc_is_a(garfield, &Animal_class); /* 1 */
|
||||||
|
ooc_is_a(garfield, &Snoopy_class); /* 0 -- the other branch */
|
||||||
|
```
|
||||||
|
|
||||||
This is the useful direction to ask in, because an upcast in C is unchecked: a
|
This is the useful direction to ask in, because an upcast in C is unchecked: a
|
||||||
caller holding an `Animal *` uses it to find out what the object really is.
|
caller holding an `Animal *` uses it to find out what the object really is.
|
||||||
|
|
||||||
|
|
@ -193,6 +270,20 @@ subclass cannot reach a base class' private field either. A class writes its
|
||||||
own fields through the struct definition, where the members are in view and no
|
own fields through the struct definition, where the members are in view and no
|
||||||
accessor is needed.
|
accessor is needed.
|
||||||
|
|
||||||
|
The rules hold at any depth. `Garfield` has a `_meals` of its own while
|
||||||
|
inheriting Cat's `_lives`, and a caller can read both by name but write neither --
|
||||||
|
the rule belongs to the declaration, not to the object:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ooc_get(garfield, "_meals"); /* readable -- Garfield's */
|
||||||
|
ooc_get(garfield, "_lives"); /* readable -- Cat's, two levels up */
|
||||||
|
ooc_set(garfield, "_meals", &n); /* refused */
|
||||||
|
ooc_set(garfield, "_lives", &n); /* also refused */
|
||||||
|
```
|
||||||
|
|
||||||
|
Private means private to the declaring class, which is why a subclass adds a
|
||||||
|
field of its own rather than leaning on the base class' private state.
|
||||||
|
|
||||||
Two things this is not. It does not keep a determined caller from reading a
|
Two things this is not. It does not keep a determined caller from reading a
|
||||||
single-underscore field, since `ooc_get()` hands back the field's real storage
|
single-underscore field, since `ooc_get()` hands back the field's real storage
|
||||||
and C has no access control -- the name is a contract, not a wall. And it does
|
and C has no access control -- the name is a contract, not a wall. And it does
|
||||||
|
|
@ -204,7 +295,8 @@ pointer.
|
||||||
|
|
||||||
include/ooc/ooc.h Public API
|
include/ooc/ooc.h Public API
|
||||||
src/ooc.c Library implementation
|
src/ooc.c Library implementation
|
||||||
example/ Complete example
|
example/ Complete example: Animal, Dog, Cat, Garfield, Snoopy
|
||||||
|
tests/safety.c Assertion suite
|
||||||
Makefile Build/install rules
|
Makefile Build/install rules
|
||||||
libooc.pc.in pkg-config template
|
libooc.pc.in pkg-config template
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,27 +164,64 @@ const ooc_class Cat_class = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create a Cat named `name` of the given `colour` and return it, or NULL.
|
* Initialise the members Cat owns, leaving the object ready to speak as a Cat.
|
||||||
*
|
*
|
||||||
* Three steps, in the order a subclass constructor needs them. The object is
|
* Split out of cat_new() so a subclass of Cat can build this part itself, the
|
||||||
* allocated through ooc_new() with a reference count of one, so the caller owns
|
* same reason animal_init() exists for Animal. Garfield does exactly that: it
|
||||||
* it and must release it. animal_init() fills in the base part, including the
|
* calls this, then installs its own vtable and adds its own members.
|
||||||
* `_id` and `__legs` that only Animal may write. Then the vtable is replaced
|
|
||||||
* with Cat's, so the object answers to Cat's speak rather than Animal's.
|
|
||||||
*
|
*
|
||||||
* The vtable is overwritten after animal_init() rather than before, since that
|
* The vtable is overwritten after animal_init() rather than before, since that
|
||||||
* call installs Animal's table and this one has to win.
|
* call installs Animal's table and this one has to win.
|
||||||
*
|
*
|
||||||
* `_lives` is set here and nowhere else. A caller can read it with ooc_get() and
|
* `_lives` is set here and nowhere else. A caller can read it with ooc_get() and
|
||||||
* cannot write it with ooc_set(), so this constructor and any future method of
|
* cannot write it with ooc_set(), so this function and any method of Cat are the
|
||||||
* Cat are the only places the value can change -- which is the point of marking
|
* only places the value can change -- which is the point of marking it with an
|
||||||
* it with an underscore.
|
* underscore.
|
||||||
*
|
*
|
||||||
* Each step can fail, and both failures release the object, which is safe
|
* The colour is copied before the base part is built, so a failed copy cannot
|
||||||
* because the destructor is already registered and frees whatever is present:
|
* leave a half-initialised object behind: on failure nothing has been written
|
||||||
* after a failed animal_init() there is no name and no colour to free, and after
|
* at all and the caller may simply release the zeroed storage. Failure leaves
|
||||||
* a failed colour copy the name has to be freed, which is exactly what the
|
* the cat unchanged.
|
||||||
* destructor does. Returns NULL if any step fails, leaving nothing to release.
|
*
|
||||||
|
* Returns 0, or -1 for a NULL cat, an already initialised one, or a failed copy.
|
||||||
|
* Requires zero-initialised members and external synchronization between
|
||||||
|
* constructors in different threads.
|
||||||
|
*/
|
||||||
|
int cat_init(Cat *cat, const char *name, int age, const char *colour)
|
||||||
|
{
|
||||||
|
char *copy;
|
||||||
|
|
||||||
|
if (!cat || cat->colour || cat->animal.vtable)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
copy = dupstr(colour);
|
||||||
|
|
||||||
|
if (!copy)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (animal_init(&cat->animal, name, age) != 0) {
|
||||||
|
free(copy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cat->colour = copy;
|
||||||
|
cat->animal.vtable = &vt;
|
||||||
|
cat->_lives = 9;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a Cat named `name` of the given `colour` and return it, or NULL.
|
||||||
|
*
|
||||||
|
* Two steps: allocate through ooc_new() with a reference count of one, so the
|
||||||
|
* caller owns the result and must release it, and then let cat_init() build the
|
||||||
|
* members. The allocation carries Cat's destructor from the start, so a failed
|
||||||
|
* cat_init() releases cleanly: nothing was written, and free(NULL) is what the
|
||||||
|
* destructor finds.
|
||||||
|
*
|
||||||
|
* Returns NULL if the allocation fails or cat_init() refuses, in both cases
|
||||||
|
* leaving nothing to release.
|
||||||
*/
|
*/
|
||||||
Cat *cat_new(const char *name, int age, const char *colour)
|
Cat *cat_new(const char *name, int age, const char *colour)
|
||||||
{
|
{
|
||||||
|
|
@ -193,20 +230,7 @@ Cat *cat_new(const char *name, int age, const char *colour)
|
||||||
if (!cat)
|
if (!cat)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
/*
|
if (cat_init(cat, name, age, colour) != 0) {
|
||||||
* Let Animal build the part it owns, including the private `_id`, then
|
|
||||||
* take over the vtable with Cat's own.
|
|
||||||
*/
|
|
||||||
if (animal_init(&cat->animal, name, age) != 0) {
|
|
||||||
ooc_release(cat);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
cat->animal.vtable = &vt;
|
|
||||||
cat->colour = dupstr(colour);
|
|
||||||
cat->_lives = 9;
|
|
||||||
|
|
||||||
if (!cat->colour) {
|
|
||||||
ooc_release(cat);
|
ooc_release(cat);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,4 +64,22 @@ extern const ooc_class Cat_class;
|
||||||
*/
|
*/
|
||||||
Cat *cat_new(const char *name, int age, const char *colour);
|
Cat *cat_new(const char *name, int age, const char *colour);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialise the members Cat owns, so a subclass constructor can build the Cat
|
||||||
|
* part of a larger object instead of duplicating it.
|
||||||
|
*
|
||||||
|
* This is the second link in the chain of constructors below Animal: a subclass
|
||||||
|
* of Cat calls this, gets Cat's vtable installed, and then overwrites the
|
||||||
|
* vtable with its own the same way cat_new() does.
|
||||||
|
*
|
||||||
|
* The caller supplies storage that ooc_new() has already allocated and
|
||||||
|
* zeroed. On failure the object is left exactly as it was found, so the caller
|
||||||
|
* may release it without a destructor having anything to do.
|
||||||
|
*
|
||||||
|
* Returns 0, or -1 for a NULL cat, an already initialised one, or a failed copy
|
||||||
|
* of the colour. Requires zero-initialised members and external synchronization
|
||||||
|
* between constructors in different threads.
|
||||||
|
*/
|
||||||
|
int cat_init(Cat *cat, const char *name, int age, const char *colour);
|
||||||
|
|
||||||
#endif /* CAT_H */
|
#endif /* CAT_H */
|
||||||
|
|
|
||||||
|
|
@ -147,22 +147,58 @@ const ooc_class Dog_class = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create a Dog named `name` of the given `breed` and return it, or NULL.
|
* Initialise the members Dog owns, leaving the object ready to speak as a Dog.
|
||||||
*
|
*
|
||||||
* Three steps, in the order a subclass constructor needs them. The object is
|
* Split out of dog_new() so a subclass of Dog can build this part itself, the
|
||||||
* allocated through ooc_new() with a reference count of one, so the caller owns
|
* same reason animal_init() exists for Animal. Snoopy does exactly that: it
|
||||||
* it and must release it. animal_init() fills in the base part, including the
|
* calls this, then installs its own vtable and adds its own members.
|
||||||
* `_id` and `__legs` that only Animal may write. Then the vtable is replaced
|
|
||||||
* with Dog's, so the object answers to Dog's speak rather than Animal's.
|
|
||||||
*
|
*
|
||||||
* The vtable is overwritten after animal_init() rather than before, since that
|
* The vtable is overwritten after animal_init() rather than before, since that
|
||||||
* call installs Animal's table and this one has to win.
|
* call installs Animal's table and this one has to win.
|
||||||
*
|
*
|
||||||
* Each step can fail, and both failures release the object, which is safe
|
* The breed is copied before the base part is built, so a failed copy cannot
|
||||||
* because the destructor is already registered and frees whatever is present:
|
* leave a half-initialised object behind: on failure nothing has been written
|
||||||
* after a failed animal_init() there is no name and no breed to free, and after
|
* at all and the caller may simply release the zeroed storage. Failure leaves
|
||||||
* a failed breed copy the name has to be freed, which is exactly what the
|
* the dog unchanged.
|
||||||
* destructor does. Returns NULL if any step fails, leaving nothing to release.
|
*
|
||||||
|
* Returns 0, or -1 for a NULL dog, an already initialised one, or a failed copy.
|
||||||
|
* Requires zero-initialised members and external synchronization between
|
||||||
|
* constructors in different threads.
|
||||||
|
*/
|
||||||
|
int dog_init(Dog *dog, const char *name, int age, const char *breed)
|
||||||
|
{
|
||||||
|
char *copy;
|
||||||
|
|
||||||
|
if (!dog || dog->breed || dog->animal.vtable)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
copy = dupstr(breed);
|
||||||
|
|
||||||
|
if (!copy)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (animal_init(&dog->animal, name, age) != 0) {
|
||||||
|
free(copy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dog->breed = copy;
|
||||||
|
dog->animal.vtable = &vt;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a Dog named `name` of the given `breed` and return it, or NULL.
|
||||||
|
*
|
||||||
|
* Two steps: allocate through ooc_new() with a reference count of one, so the
|
||||||
|
* caller owns the result and must release it, and then let dog_init() build the
|
||||||
|
* members. The allocation carries Dog's destructor from the start, so a failed
|
||||||
|
* dog_init() releases cleanly: nothing was written, and free(NULL) is what the
|
||||||
|
* destructor finds.
|
||||||
|
*
|
||||||
|
* Returns NULL if the allocation fails or dog_init() refuses, in both cases
|
||||||
|
* leaving nothing to release.
|
||||||
*/
|
*/
|
||||||
Dog *dog_new(const char *name, int age, const char *breed)
|
Dog *dog_new(const char *name, int age, const char *breed)
|
||||||
{
|
{
|
||||||
|
|
@ -171,19 +207,7 @@ Dog *dog_new(const char *name, int age, const char *breed)
|
||||||
if (!dog)
|
if (!dog)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
/*
|
if (dog_init(dog, name, age, breed) != 0) {
|
||||||
* Let Animal build the part it owns, including the private `_id`, then
|
|
||||||
* take over the vtable with Dog's own.
|
|
||||||
*/
|
|
||||||
if (animal_init(&dog->animal, name, age) != 0) {
|
|
||||||
ooc_release(dog);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
dog->animal.vtable = &vt;
|
|
||||||
dog->breed = dupstr(breed);
|
|
||||||
|
|
||||||
if (!dog->breed) {
|
|
||||||
ooc_release(dog);
|
ooc_release(dog);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,22 @@ extern const ooc_class Dog_class;
|
||||||
*/
|
*/
|
||||||
Dog *dog_new(const char *name, int age, const char *breed);
|
Dog *dog_new(const char *name, int age, const char *breed);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialise the members Dog owns, so a subclass constructor can build the Dog
|
||||||
|
* part of a larger object instead of duplicating it.
|
||||||
|
*
|
||||||
|
* This is the second link in the chain of constructors below Animal: a subclass
|
||||||
|
* of Dog calls this, gets Dog's vtable installed, and then overwrites the
|
||||||
|
* vtable with its own the same way dog_new() does.
|
||||||
|
*
|
||||||
|
* The caller supplies storage that ooc_new() has already allocated and
|
||||||
|
* zeroed. On failure the object is left exactly as it was found, so the caller
|
||||||
|
* may release it without a destructor having anything to do.
|
||||||
|
*
|
||||||
|
* Returns 0, or -1 for a NULL dog, an already initialised one, or a failed copy
|
||||||
|
* of the breed. Requires zero-initialised members and external synchronization
|
||||||
|
* between constructors in different threads.
|
||||||
|
*/
|
||||||
|
int dog_init(Dog *dog, const char *name, int age, const char *breed);
|
||||||
|
|
||||||
#endif /* DOG_H */
|
#endif /* DOG_H */
|
||||||
|
|
|
||||||
281
example/garfield.c
Normal file
281
example/garfield.c
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
/*
|
||||||
|
* This file is part of libooc.
|
||||||
|
* https://xw3.org/hanez/libooc
|
||||||
|
*
|
||||||
|
* Copyright 2026 Johannes Findeisen <you@hanez.org>
|
||||||
|
* Licensed under the terms of the Apache-2.0 license.
|
||||||
|
* https://opensource.org/license/apache-2-0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Garfield -- a subclass of Cat, and so a subclass of a subclass.
|
||||||
|
*
|
||||||
|
* Everything a first-level subclass does, this file does again one level down:
|
||||||
|
* the object is allocated from Garfield_class, whose `super` is Cat_class so
|
||||||
|
* field lookup continues through Cat to Animal; the members of the classes above
|
||||||
|
* are built by cat_init() rather than duplicated; and a separate vtable and
|
||||||
|
* destructor are registered for Garfield, so the override and the extra
|
||||||
|
* allocation are cleaned up by the object's runtime type.
|
||||||
|
*
|
||||||
|
* Adding a level turns out to change none of the rules, which is the point worth
|
||||||
|
* making with an example. Ownership, the underscore conventions, the virtual
|
||||||
|
* destructor and the chain walk all behave identically whether the base is two
|
||||||
|
* levels up or one. What does change is the destructor, and that is the part
|
||||||
|
* worth reading closely: it now frees three strings instead of two, and forgets
|
||||||
|
* the middle one at its peril.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "garfield.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copy `text` onto the heap; the caller owns the result.
|
||||||
|
*
|
||||||
|
* A private copy of the same helper animal.c, cat.c and dog.c each keep, and
|
||||||
|
* identical to all of them. By now the duplication is the point being made
|
||||||
|
* rather than a detail worth hiding: four identical copies is where a real
|
||||||
|
* codebase would stop and either publish the helper or leave it to the class
|
||||||
|
* that owns it. The library is too small to have an opinion, and an example is
|
||||||
|
* clearer for showing what each class is actually responsible for.
|
||||||
|
*
|
||||||
|
* Returns NULL if `text` is NULL or the allocation fails. The size is checked
|
||||||
|
* before the terminator is added, so a string long enough to wrap cannot ask
|
||||||
|
* malloc() for a short buffer.
|
||||||
|
*/
|
||||||
|
static char *dupstr(const char *text)
|
||||||
|
{
|
||||||
|
size_t len;
|
||||||
|
char *copy;
|
||||||
|
|
||||||
|
if (!text)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
len = strlen(text);
|
||||||
|
if (len == SIZE_MAX)
|
||||||
|
return NULL;
|
||||||
|
++len;
|
||||||
|
|
||||||
|
copy = malloc(len);
|
||||||
|
if (copy)
|
||||||
|
memcpy(copy, text, len);
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Virtual destructor: releases everything garfield_new() allocated.
|
||||||
|
*
|
||||||
|
* The library calls the destructor belonging to the object's runtime type and
|
||||||
|
* stops there, so this one owns the whole hierarchy and not merely Garfield's
|
||||||
|
* own member. Three frees are needed where Cat needed two: `favourite_food` is
|
||||||
|
* Garfield's, `colour` was allocated by cat_init() and `name` by animal_init()
|
||||||
|
* before that, and neither Cat's destroy() nor Animal's ever runs for a
|
||||||
|
* Garfield.
|
||||||
|
*
|
||||||
|
* Omitting the middle free is the mistake a third level invites, and it is
|
||||||
|
* silent: the object still looks fine, and the leak only shows up in a memory
|
||||||
|
* checker. Forgetting `name` on the way down would have the same effect one
|
||||||
|
* level earlier.
|
||||||
|
*
|
||||||
|
* The order does not matter, since the three allocations are independent, and
|
||||||
|
* all three fields are marked as owned in their own class' table -- so ooc_set()
|
||||||
|
* has already released a previous value where there was one, and what remains
|
||||||
|
* here is freed on destruction.
|
||||||
|
*/
|
||||||
|
static void destroy(ooc_object *object)
|
||||||
|
{
|
||||||
|
Garfield *garfield = (Garfield *)object;
|
||||||
|
|
||||||
|
free(garfield->favourite_food);
|
||||||
|
free(garfield->cat.colour);
|
||||||
|
free(garfield->cat.animal.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Garfield's implementation of AnimalVTable::speak.
|
||||||
|
*
|
||||||
|
* The signature is Animal's, not Garfield's, and the cast back down is the same
|
||||||
|
* no-op at every level, since Animal sits at offset zero however many classes
|
||||||
|
* are embedded above it. That is what makes a deep hierarchy cheap: no
|
||||||
|
* adjustment, no pointer arithmetic, and the cast is valid because the object
|
||||||
|
* really was a Garfield.
|
||||||
|
*
|
||||||
|
* The members come from three different classes in one printf() -- the name from
|
||||||
|
* Animal, the colour and the lives count from Cat, the food and the meals from
|
||||||
|
* Garfield -- which is the clearest statement of what inheritance bought: an
|
||||||
|
* override with access to the whole hierarchy behind it.
|
||||||
|
*
|
||||||
|
* The fallbacks keep a cleared string from reaching printf(). A field set to
|
||||||
|
* NULL is a legitimate state, ooc_set(garfield, "favourite_food", NULL) being
|
||||||
|
* allowed to succeed, and a vtable entry is called with whatever the object
|
||||||
|
* currently holds.
|
||||||
|
*/
|
||||||
|
static void speak(Animal *animal)
|
||||||
|
{
|
||||||
|
Garfield *garfield = (Garfield *)animal;
|
||||||
|
|
||||||
|
printf("%s says: Meow! (%s, %s, meal %d, %d lives left)\n",
|
||||||
|
garfield->cat.animal.name ? garfield->cat.animal.name : "(unnamed)",
|
||||||
|
garfield->cat.colour ? garfield->cat.colour : "(unknown colour)",
|
||||||
|
garfield->favourite_food ? garfield->favourite_food
|
||||||
|
: "(no favourite food)",
|
||||||
|
garfield->_meals,
|
||||||
|
garfield->cat._lives);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Garfield's own vtable, holding Garfield's speak.
|
||||||
|
*
|
||||||
|
* Identical in shape to the tables in animal.c, cat.c and dog.c. A vtable slot
|
||||||
|
* is chosen by the class that owns the table, so a Garfield reaches this speak
|
||||||
|
* and never Cat's, even though Cat declared the same method -- the override
|
||||||
|
* holds all the way down.
|
||||||
|
*/
|
||||||
|
static const AnimalVTable vt = {
|
||||||
|
.speak = speak,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The one field Garfield adds on top of the ones above it.
|
||||||
|
*
|
||||||
|
* A derived class lists only what it declares itself. Everything else --
|
||||||
|
* "colour" and "_lives" from Cat, "name", "age", "_id" and "__legs" from Animal
|
||||||
|
* -- resolves by walking up from Garfield_class through Cat_class to
|
||||||
|
* Animal_class. Inserting a class in the middle hides nothing.
|
||||||
|
*
|
||||||
|
* `favourite_food` is marked as owned, exactly as Cat's colour and Dog's breed
|
||||||
|
* are, so replacing it through ooc_set() releases the string it held before.
|
||||||
|
* `_meals` carries a leading underscore, so ooc_set() refuses to write it while
|
||||||
|
* ooc_get() still returns it, and the rule applies to a field this class
|
||||||
|
* declared rather than one it inherited.
|
||||||
|
*/
|
||||||
|
static const ooc_field Garfield_fields[] = {
|
||||||
|
{ "favourite_food", offsetof(Garfield, favourite_food),
|
||||||
|
sizeof(((Garfield *)0)->favourite_food), 1 },
|
||||||
|
{ "_meals", offsetof(Garfield, _meals),
|
||||||
|
sizeof(((Garfield *)0)->_meals), 0 },
|
||||||
|
{ NULL, 0, 0, 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Garfield's runtime type record.
|
||||||
|
*
|
||||||
|
* `super` is Cat_class, and that single pointer is what makes this a subclass of
|
||||||
|
* a subclass: ooc_new() writes this record's address into every object it
|
||||||
|
* allocates, and ooc_get(), ooc_set() and ooc_is_a() all start from there and
|
||||||
|
* walk outwards, reaching Cat's and Animal's fields through it.
|
||||||
|
*
|
||||||
|
* `size` is sizeof(Garfield), which must hold Cat and the food on top. The
|
||||||
|
* runtime rejects a class whose base is larger, since the base's fields would
|
||||||
|
* then be written past the end of the allocation.
|
||||||
|
*
|
||||||
|
* The record is a file-scope constant, as it is for every class.
|
||||||
|
*/
|
||||||
|
const ooc_class Garfield_class = {
|
||||||
|
.size = sizeof(Garfield),
|
||||||
|
.destroy = destroy,
|
||||||
|
.super = &Cat_class,
|
||||||
|
.fields = Garfield_fields,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialise the members Garfield owns, leaving the object ready to speak as a
|
||||||
|
* Garfield.
|
||||||
|
*
|
||||||
|
* The third link in the chain of constructors, and it is a strict composition
|
||||||
|
* of the two below it. cat_init() builds the Cat part -- which calls
|
||||||
|
* animal_init() for the Animal part -- and Garfield overwrites the vtable
|
||||||
|
* afterwards, since cat_init() installs Cat's table and Garfield's has to win.
|
||||||
|
*
|
||||||
|
* The food is copied first, for the same reason cat_init() copies the colour
|
||||||
|
* before calling animal_init(): a failure here cannot leave a partly built
|
||||||
|
* object behind, so the caller may release the zeroed storage without the
|
||||||
|
* destructor having anything to do.
|
||||||
|
*
|
||||||
|
* `_meals` is set here and nowhere else, so it changes only through this
|
||||||
|
* constructor or a future method of Garfield. A caller can read it by name and
|
||||||
|
* cannot write it, which is the contract the underscore asks for.
|
||||||
|
*
|
||||||
|
* Returns 0, or -1 for a NULL object, an already initialised one, or a failed
|
||||||
|
* copy. Requires zero-initialised members and external synchronization between
|
||||||
|
* constructors in different threads.
|
||||||
|
*/
|
||||||
|
int garfield_init(Garfield *garfield, const char *name, int age,
|
||||||
|
const char *colour, const char *favourite_food)
|
||||||
|
{
|
||||||
|
char *copy;
|
||||||
|
|
||||||
|
if (!garfield || garfield->favourite_food || garfield->cat.colour ||
|
||||||
|
garfield->cat.animal.vtable)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
copy = dupstr(favourite_food);
|
||||||
|
|
||||||
|
if (!copy)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Let Cat build the part it owns -- which lets Animal build the part it
|
||||||
|
* owns -- then take over the vtable with Garfield's own.
|
||||||
|
*/
|
||||||
|
if (cat_init(&garfield->cat, name, age, colour) != 0) {
|
||||||
|
free(copy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
garfield->favourite_food = copy;
|
||||||
|
garfield->cat.animal.vtable = &vt;
|
||||||
|
garfield->_meals = 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a Garfield named `name`, of the given `colour` and `favourite_food`,
|
||||||
|
* and return it, or NULL.
|
||||||
|
*
|
||||||
|
* Two steps, as in cat_new() and dog_new(): allocate through ooc_new() with a
|
||||||
|
* reference count of one, so the caller owns the result and must release it,
|
||||||
|
* and then let garfield_init() build the members. The allocation carries
|
||||||
|
* Garfield's destructor from the start, which frees the whole hierarchy, so a
|
||||||
|
* failed init releases cleanly -- on every failure path above, nothing was
|
||||||
|
* written and free(NULL) is what the destructor finds.
|
||||||
|
*
|
||||||
|
* Returns NULL if the allocation fails or garfield_init() refuses, in both cases
|
||||||
|
* leaving nothing to release.
|
||||||
|
*/
|
||||||
|
Garfield *garfield_new(const char *name, int age, const char *colour,
|
||||||
|
const char *favourite_food)
|
||||||
|
{
|
||||||
|
Garfield *garfield = ooc_new(&Garfield_class);
|
||||||
|
|
||||||
|
if (!garfield)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (garfield_init(garfield, name, age, colour, favourite_food) != 0) {
|
||||||
|
ooc_release(garfield);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return garfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read back `_meals`, the field ooc_get() also returns.
|
||||||
|
*
|
||||||
|
* Takes a const object, so it can be called where ooc_set() could not -- a
|
||||||
|
* caller with a const pointer can still be told the count.
|
||||||
|
*
|
||||||
|
* Returns 0 for a NULL object, which is indistinguishable from a real count of
|
||||||
|
* zero, so a caller that has to tell the two apart should check the pointer
|
||||||
|
* first. The count starts at 1.
|
||||||
|
*/
|
||||||
|
int garfield_meals(const Garfield *garfield)
|
||||||
|
{
|
||||||
|
return garfield ? garfield->_meals : 0;
|
||||||
|
}
|
||||||
122
example/garfield.h
Normal file
122
example/garfield.h
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* This file is part of libooc.
|
||||||
|
* https://xw3.org/hanez/libooc
|
||||||
|
*
|
||||||
|
* Copyright 2026 Johannes Findeisen <you@hanez.org>
|
||||||
|
* Licensed under the terms of the Apache-2.0 license.
|
||||||
|
* https://opensource.org/license/apache-2-0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef GARFIELD_H
|
||||||
|
#define GARFIELD_H
|
||||||
|
|
||||||
|
#include "cat.h"
|
||||||
|
|
||||||
|
typedef struct Garfield Garfield;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A subclass of a subclass, three levels below the root.
|
||||||
|
*
|
||||||
|
* The embedded Cat comes first, so the same two properties hold one level down
|
||||||
|
* from the case dog.h describes: the ooc_object header stays at offset zero
|
||||||
|
* where the library looks for it, and the upcast from Garfield * to Animal * is
|
||||||
|
* a no-op cast because every address is the same. The cost is unchanged too --
|
||||||
|
* no member may go before the base.
|
||||||
|
*
|
||||||
|
* Garfield adds a favourite food of its own and nothing that shadows Cat or
|
||||||
|
* Animal, so every inherited field keeps the meaning it had. "colour" and
|
||||||
|
* "_lives" still resolve to Cat's storage, and the underscore rules still apply
|
||||||
|
* to the class that declared them: a caller can read "_lives" by name and cannot
|
||||||
|
* write it, even though Garfield sits between the caller and the declaration.
|
||||||
|
*
|
||||||
|
* The struct holds no destructor of its own; the one Garfield registers lives in
|
||||||
|
* Garfield_class and releases all three strings in the hierarchy -- the food,
|
||||||
|
* Cat's colour and Animal's name.
|
||||||
|
*/
|
||||||
|
struct Garfield {
|
||||||
|
Cat cat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Garfield's own field, and the only one it adds. Marked as owned in the
|
||||||
|
* field table, so ooc_set() releases the string it displaces exactly as it
|
||||||
|
* does for a Dog's breed or a Cat's colour. A third level of subclassing
|
||||||
|
* changes nothing about how ownership behaves.
|
||||||
|
*/
|
||||||
|
char *favourite_food;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A field nobody but this class may change, counted in meals rather than
|
||||||
|
* lives. One underscore hides it from ooc_set() and leaves it readable, and
|
||||||
|
* garfield_meals() is the way a caller asks politely.
|
||||||
|
*
|
||||||
|
* Worth noting that `Cat` has a `_lives` of its own and Garfield adds a
|
||||||
|
* second private field rather than reusing it. Private means private to the
|
||||||
|
* declaring class, so a subclass cannot lean on the base class' private
|
||||||
|
* state even from inside the same hierarchy -- which is why the two fields
|
||||||
|
* coexist under different names.
|
||||||
|
*/
|
||||||
|
int _meals;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Garfield's runtime type record, the value ooc_new() takes.
|
||||||
|
*
|
||||||
|
* Its `super` is Cat_class rather than Animal_class, which is the whole of a
|
||||||
|
* third level: field lookup starts here, continues at Cat_class for "colour" and
|
||||||
|
* "_lives", and reaches Animal_class for "name", "age", "_id" and "__legs". The
|
||||||
|
* chain is walked as far as it goes, so nothing from an ancestor is lost by
|
||||||
|
* inserting a class in between.
|
||||||
|
*
|
||||||
|
* Its `size` is sizeof(Garfield), which has to hold Cat and Garfield's own
|
||||||
|
* member on top, and the runtime rejects a class smaller than its base.
|
||||||
|
*/
|
||||||
|
extern const ooc_class Garfield_class;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a Garfield named `name`, of the given `colour` and `favourite_food`,
|
||||||
|
* and return it, or NULL.
|
||||||
|
*
|
||||||
|
* The result has a reference count of one, so ownership is the caller's and it
|
||||||
|
* must reach ooc_release() eventually. Returns NULL if any step of the
|
||||||
|
* construction fails, leaving nothing to release.
|
||||||
|
*
|
||||||
|
* The object is a Garfield and answers to Garfield's `speak` however it is later
|
||||||
|
* reached -- through a Garfield *, a Cat * or an Animal * alike.
|
||||||
|
*/
|
||||||
|
Garfield *garfield_new(const char *name, int age, const char *colour,
|
||||||
|
const char *favourite_food);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialise the members Garfield owns, so a subclass of Garfield could build
|
||||||
|
* this part for itself.
|
||||||
|
*
|
||||||
|
* The third link in the chain of constructors: garfield_init() composes
|
||||||
|
* cat_init(), which composes animal_init(). Each one installs its own vtable and
|
||||||
|
* the one above overwrites it, so the object ends up speaking as the most
|
||||||
|
* derived class.
|
||||||
|
*
|
||||||
|
* The caller supplies storage that ooc_new() has already allocated and zeroed.
|
||||||
|
* On failure the object is left exactly as it was found, so the caller may
|
||||||
|
* release it without a destructor having anything to do.
|
||||||
|
*
|
||||||
|
* Returns 0, or -1 for a NULL object, an already initialised one, or a failed
|
||||||
|
* copy. Requires zero-initialised members and external synchronization between
|
||||||
|
* constructors in different threads.
|
||||||
|
*/
|
||||||
|
int garfield_init(Garfield *garfield, const char *name, int age,
|
||||||
|
const char *colour, const char *favourite_food);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read back `_meals`, the field ooc_get() also returns.
|
||||||
|
*
|
||||||
|
* The accessor a class provides for a value it keeps to itself. A single
|
||||||
|
* underscore leaves the field readable by name, so this is a courtesy rather
|
||||||
|
* than a necessity, unlike animal_legs() for the two-underscore `__legs`.
|
||||||
|
*
|
||||||
|
* Returns 0 for a NULL object, which is indistinguishable from a real count of
|
||||||
|
* zero, so a caller that has to tell the two apart should check the pointer
|
||||||
|
* first.
|
||||||
|
*/
|
||||||
|
int garfield_meals(const Garfield *garfield);
|
||||||
|
|
||||||
|
#endif /* GARFIELD_H */
|
||||||
343
example/main.c
343
example/main.c
|
|
@ -8,7 +8,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks through the libooc life cycle twice over, once per subclass.
|
* Walks through the libooc life cycle four times, once per class in the
|
||||||
|
* hierarchy.
|
||||||
*
|
*
|
||||||
* The Dog walk covers the whole API: allocation, a second reference through a
|
* The Dog walk covers the whole API: allocation, a second reference through a
|
||||||
* base class, reading and writing fields by name, the private field rules, and
|
* base class, reading and writing fields by name, the private field rules, and
|
||||||
|
|
@ -16,10 +17,18 @@
|
||||||
* second subclass, so the output makes the one thing that is not visible in the
|
* second subclass, so the output makes the one thing that is not visible in the
|
||||||
* source clear: `animal_speak` is a single call site and still reaches Dog's
|
* source clear: `animal_speak` is a single call site and still reaches Dog's
|
||||||
* implementation for one object and Cat's for the other.
|
* implementation for one object and Cat's for the other.
|
||||||
|
*
|
||||||
|
* Garfield and Snoopy go one level deeper, each on its own branch, and they are
|
||||||
|
* what turn a list of classes into a hierarchy worth looking at. The last walk
|
||||||
|
* puts all four objects through one Animal * and asks what each really is, which
|
||||||
|
* is the question a caller with no static type information can only answer by
|
||||||
|
* asking.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "cat.h"
|
#include "cat.h"
|
||||||
#include "dog.h"
|
#include "dog.h"
|
||||||
|
#include "garfield.h"
|
||||||
|
#include "snoopy.h"
|
||||||
|
|
||||||
#include <ooc/ooc.h>
|
#include <ooc/ooc.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
@ -27,15 +36,32 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
/* Copy `text` onto the heap, because a field that owns its value needs heap. */
|
/*
|
||||||
|
* Copy `text` onto the heap, because a field that owns its value needs heap.
|
||||||
|
*
|
||||||
|
* The example classes have their own private copies of this; it is duplicated
|
||||||
|
* here rather than shared because main.c is a caller, not part of the hierarchy,
|
||||||
|
* and has no business reaching into a class' internals for it.
|
||||||
|
*
|
||||||
|
* Returns NULL if `text` is NULL or the allocation fails.
|
||||||
|
*/
|
||||||
static char *copy_of(const char *text)
|
static char *copy_of(const char *text)
|
||||||
{
|
{
|
||||||
size_t len = strlen(text);
|
size_t len;
|
||||||
char *copy = malloc(len + 1);
|
char *copy;
|
||||||
++len;
|
|
||||||
|
if (!text)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
len = strlen(text);
|
||||||
|
|
||||||
|
if (len == SIZE_MAX)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
copy = malloc(len + 1);
|
||||||
|
|
||||||
if (copy)
|
if (copy)
|
||||||
memcpy(copy, text, len);
|
memcpy(copy, text, len + 1);
|
||||||
|
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
@ -260,6 +286,302 @@ fail:
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The Garfield walk: a subclass of a subclass.
|
||||||
|
*
|
||||||
|
* Nothing here is new in kind. The object is created and released the same way
|
||||||
|
* as a Dog, fields resolve by name the same way, and the ownership and underscore
|
||||||
|
* rules are the library's rather than the class's. The differences are all
|
||||||
|
* consequences of the extra level, and they are what the walk is for.
|
||||||
|
*
|
||||||
|
* Lookup now crosses two class records instead of one. "favourite_food" and
|
||||||
|
* "_meals" are Garfield's own, "colour" and "_lives" are found at Cat_class, and
|
||||||
|
* "name" and "age" are found at Animal_class -- so a name declared two levels up
|
||||||
|
* is as reachable as one declared immediately above.
|
||||||
|
*
|
||||||
|
* The private rules also cross the level unchanged, and that is worth seeing:
|
||||||
|
* `_meals` belongs to Garfield, `_lives` to Cat, and a caller can read both by
|
||||||
|
* name but write neither. A subclass sitting between the caller and the
|
||||||
|
* declaration does not weaken the rule.
|
||||||
|
*
|
||||||
|
* Returns 0 when the walk completes and -1 if a step the example relies on was
|
||||||
|
* refused.
|
||||||
|
*/
|
||||||
|
static int visit_garfield(void)
|
||||||
|
{
|
||||||
|
Garfield *garfield;
|
||||||
|
Cat *cat_view;
|
||||||
|
Animal *animal;
|
||||||
|
char *food;
|
||||||
|
int servings = 7;
|
||||||
|
|
||||||
|
garfield = garfield_new("Garfield", 4, "orange", "lasagna");
|
||||||
|
if (!garfield) {
|
||||||
|
fprintf(stderr, "failed to create garfield\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Upcasting is unchecked in C, so these two casts are just arithmetic that
|
||||||
|
* happens to be zero: every class in the chain embeds the one above it as
|
||||||
|
* its first member. A handle to the middle of the chain works exactly like a
|
||||||
|
* handle to the top or the bottom, which is why the same lookup and the same
|
||||||
|
* vtable work through any of them.
|
||||||
|
*/
|
||||||
|
cat_view = &garfield->cat;
|
||||||
|
animal = &garfield->cat.animal;
|
||||||
|
|
||||||
|
printf("through a Cat handle, the same object answers as %s.\n",
|
||||||
|
*(char **)ooc_get(cat_view, "name"));
|
||||||
|
|
||||||
|
printf("%s is a %d year old %s %s who has eaten %d meal%s.\n",
|
||||||
|
*(char **)ooc_get(garfield, "name"),
|
||||||
|
*(int *)ooc_get(garfield, "age"),
|
||||||
|
*(char **)ooc_get(garfield, "colour"),
|
||||||
|
*(char **)ooc_get(garfield, "favourite_food"),
|
||||||
|
garfield_meals(garfield),
|
||||||
|
garfield_meals(garfield) == 1 ? "" : "s");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The owned field is the class's own, so replacing it hands the old string
|
||||||
|
* over and frees it -- the same rule as a breed or a colour, one level down.
|
||||||
|
*/
|
||||||
|
food = copy_of("pizza");
|
||||||
|
if (!food || ooc_set(garfield, "favourite_food", &food) != 0) {
|
||||||
|
fprintf(stderr, "failed to set favourite food\n");
|
||||||
|
free(food);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("%s now prefers %s.\n", *(char **)ooc_get(garfield, "name"),
|
||||||
|
*(char **)ooc_get(garfield, "favourite_food"));
|
||||||
|
|
||||||
|
/* Both private fields are readable and neither is writable. */
|
||||||
|
if (ooc_set(garfield, "_meals", &servings) != 0)
|
||||||
|
printf("_meals is private, and reads back as %d\n",
|
||||||
|
*(int *)ooc_get(garfield, "_meals"));
|
||||||
|
if (ooc_set(garfield, "_lives", &servings) != 0)
|
||||||
|
printf("_lives is Cat's, and reads back as %d\n",
|
||||||
|
*(int *)ooc_get(garfield, "_lives"));
|
||||||
|
printf("__legs is readable from three levels down: %s\n",
|
||||||
|
ooc_get(garfield, "__legs") ? "yes" : "no");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The exact type is the deepest class record, reached without a walk, and
|
||||||
|
* the subtype test walks all three links to say the same thing the long way.
|
||||||
|
*/
|
||||||
|
printf("a garfield is exactly a Garfield: %s, a Cat: %s, an Animal: %s\n",
|
||||||
|
garfield->cat.animal.object.class == &Garfield_class ? "yes" : "no",
|
||||||
|
ooc_is_a(garfield, &Cat_class) ? "yes" : "no",
|
||||||
|
ooc_is_a(garfield, &Animal_class) ? "yes" : "no");
|
||||||
|
|
||||||
|
/* The other branch of the hierarchy is not reachable from here. */
|
||||||
|
printf("a garfield is a Snoopy: %s, and has an imagination: %s\n",
|
||||||
|
ooc_is_a(garfield, &Snoopy_class) ? "yes" : "no",
|
||||||
|
ooc_get(garfield, "imagination") ? "yes" : "no");
|
||||||
|
|
||||||
|
/* One call site, and the vtable decides which speak() runs. */
|
||||||
|
animal_speak(animal);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One reference only, despite the three handles to it: cat_view and animal
|
||||||
|
* alias the same storage, so releasing any one of them twice would destroy
|
||||||
|
* the object early. A pointer to a base class is a view, not a new claim.
|
||||||
|
*/
|
||||||
|
ooc_release(garfield);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
ooc_release(garfield);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The Snoopy walk: the other branch, and a hidden field one level down.
|
||||||
|
*
|
||||||
|
* The same calls as visit_garfield(), against a Dog subclass rather than a Cat
|
||||||
|
* one, so the differences are the ones the fork causes. `__flights` carries two
|
||||||
|
* underscores, which ooc_get() withholds as well as ooc_set() refusing to
|
||||||
|
* write it, so snoopy_flights() is the only way in -- where Garfield's `_meals`
|
||||||
|
* is still readable by name.
|
||||||
|
*
|
||||||
|
* Returns 0 when the walk completes and -1 if a step the example relies on was
|
||||||
|
* refused.
|
||||||
|
*/
|
||||||
|
static int visit_snoopy(void)
|
||||||
|
{
|
||||||
|
Snoopy *snoopy;
|
||||||
|
Animal *animal;
|
||||||
|
char *dream;
|
||||||
|
int hours = 12;
|
||||||
|
|
||||||
|
snoopy = snoopy_new("Snoopy", 3, "beagle", "flying his red baron");
|
||||||
|
if (!snoopy) {
|
||||||
|
fprintf(stderr, "failed to create snoopy\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
animal = &snoopy->dog.animal;
|
||||||
|
|
||||||
|
printf("%s is a %d year old %s dreaming of %s, with %d flights.\n",
|
||||||
|
*(char **)ooc_get(snoopy, "name"),
|
||||||
|
*(int *)ooc_get(snoopy, "age"),
|
||||||
|
*(char **)ooc_get(snoopy, "breed"),
|
||||||
|
*(char **)ooc_get(snoopy, "imagination"),
|
||||||
|
snoopy_flights(snoopy));
|
||||||
|
|
||||||
|
/* The owned field again, this time one declared by a Dog subclass. */
|
||||||
|
dream = copy_of("a chicken dinner");
|
||||||
|
if (!dream || ooc_set(snoopy, "imagination", &dream) != 0) {
|
||||||
|
fprintf(stderr, "failed to set imagination\n");
|
||||||
|
free(dream);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("%s is now dreaming of %s.\n", *(char **)ooc_get(snoopy, "name"),
|
||||||
|
*(char **)ooc_get(snoopy, "imagination"));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Two underscores, so there is no way in by name in either direction. The
|
||||||
|
* class reports the value itself instead, which is the whole point of hiding
|
||||||
|
* it: the caller is told what it may know and nothing more.
|
||||||
|
*/
|
||||||
|
printf("__flights is readable by name: %s, writable: %s\n",
|
||||||
|
ooc_get(snoopy, "__flights") ? "yes" : "no",
|
||||||
|
ooc_set(snoopy, "__flights", &hours) == 0 ? "yes" : "no");
|
||||||
|
printf("and the class still reports %d flights.\n",
|
||||||
|
snoopy_flights(snoopy));
|
||||||
|
|
||||||
|
printf("a snoopy is a Dog: %s, a Cat: %s, a Garfield: %s\n",
|
||||||
|
ooc_is_a(snoopy, &Dog_class) ? "yes" : "no",
|
||||||
|
ooc_is_a(snoopy, &Cat_class) ? "yes" : "no",
|
||||||
|
ooc_is_a(snoopy, &Garfield_class) ? "yes" : "no");
|
||||||
|
|
||||||
|
animal_speak(animal);
|
||||||
|
|
||||||
|
ooc_release(snoopy);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
ooc_release(snoopy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The hierarchy walk: four objects, one pointer type, no static information.
|
||||||
|
*
|
||||||
|
* This is what the runtime is for. Each object is created as its own class and
|
||||||
|
* then handled only as an Animal *, with the pointer's type carrying nothing at
|
||||||
|
* all about what it points to. The single animal_speak() call below reaches four
|
||||||
|
* different implementations, and the ooc_is_a() answers come from the objects
|
||||||
|
* themselves rather than from the call site.
|
||||||
|
*
|
||||||
|
* The order is the hierarchy: a root, then a class on each branch, then the two
|
||||||
|
* subclasses of a subclass. Reading the output top to bottom shows what a
|
||||||
|
* three-level chain does to a call that knows none of it.
|
||||||
|
*
|
||||||
|
* Returns 0 when the walk completes.
|
||||||
|
*/
|
||||||
|
static int visit_hierarchy(void)
|
||||||
|
{
|
||||||
|
Animal *rex;
|
||||||
|
Animal *mia;
|
||||||
|
Animal *garfield;
|
||||||
|
Animal *snoopy;
|
||||||
|
|
||||||
|
printf("\n-- one pointer type, four runtime types --\n");
|
||||||
|
|
||||||
|
/* Each object is created as its own class and only used as an Animal. */
|
||||||
|
rex = (Animal *)dog_new("Rex", 5, "German Shepherd");
|
||||||
|
mia = (Animal *)cat_new("Mia", 3, "tabby");
|
||||||
|
garfield = (Animal *)garfield_new("Garfield", 4, "orange", "lasagna");
|
||||||
|
snoopy = (Animal *)snoopy_new("Snoopy", 3, "beagle", "his red baron");
|
||||||
|
|
||||||
|
if (!rex || !mia || !garfield || !snoopy) {
|
||||||
|
fprintf(stderr, "failed to create the hierarchy\n");
|
||||||
|
|
||||||
|
/* Release whatever did get built, since each reference is its own. */
|
||||||
|
ooc_release(rex);
|
||||||
|
ooc_release(mia);
|
||||||
|
ooc_release(garfield);
|
||||||
|
ooc_release(snoopy);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Four calls to one function. Which speak() runs is decided by the vtable
|
||||||
|
* each object carries, and the pointer type plays no part in it -- the whole
|
||||||
|
* of the polymorphism is visible right here in the output.
|
||||||
|
*/
|
||||||
|
animal_speak(rex);
|
||||||
|
animal_speak(mia);
|
||||||
|
animal_speak(garfield);
|
||||||
|
animal_speak(snoopy);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* And the runtime type can be recovered from the object alone. This is the
|
||||||
|
* question a caller with an Animal * and no static information has to ask,
|
||||||
|
* and the chain walk answers it for a base class as readily as for the exact
|
||||||
|
* type -- which is the direction that is actually useful.
|
||||||
|
*/
|
||||||
|
printf("\n%s is a Dog: %d, a Cat: %d, an Animal: %d\n",
|
||||||
|
*(char **)ooc_get(rex, "name"),
|
||||||
|
ooc_is_a(rex, &Dog_class),
|
||||||
|
ooc_is_a(rex, &Cat_class),
|
||||||
|
ooc_is_a(rex, &Animal_class));
|
||||||
|
|
||||||
|
printf("%s is a Dog: %d, a Cat: %d, a Garfield: %d\n",
|
||||||
|
*(char **)ooc_get(garfield, "name"),
|
||||||
|
ooc_is_a(garfield, &Dog_class),
|
||||||
|
ooc_is_a(garfield, &Cat_class),
|
||||||
|
ooc_is_a(garfield, &Garfield_class));
|
||||||
|
|
||||||
|
printf("%s is a Snoopy: %d, a Dog: %d, a Cat: %d\n",
|
||||||
|
*(char **)ooc_get(snoopy, "name"),
|
||||||
|
ooc_is_a(snoopy, &Snoopy_class),
|
||||||
|
ooc_is_a(snoopy, &Dog_class),
|
||||||
|
ooc_is_a(snoopy, &Cat_class));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A field declared on one branch is not reachable from the other. Both
|
||||||
|
* names are absent from both chains, which is why both calls return NULL and
|
||||||
|
* why asking for the wrong branch's field is safe rather than corrupting
|
||||||
|
* memory at a bogus offset.
|
||||||
|
*/
|
||||||
|
printf("\n%s has an imagination: %s, and a colour: %s\n",
|
||||||
|
*(char **)ooc_get(snoopy, "name"),
|
||||||
|
ooc_get(snoopy, "imagination") ? "yes" : "no",
|
||||||
|
ooc_get(snoopy, "colour") ? "yes" : "no");
|
||||||
|
|
||||||
|
printf("%s has a favourite food: %s, and a breed: %s\n",
|
||||||
|
*(char **)ooc_get(garfield, "name"),
|
||||||
|
ooc_get(garfield, "favourite_food") ? "yes" : "no",
|
||||||
|
ooc_get(garfield, "breed") ? "yes" : "no");
|
||||||
|
|
||||||
|
/* One reference each, so four releases. */
|
||||||
|
ooc_release(rex);
|
||||||
|
ooc_release(mia);
|
||||||
|
ooc_release(garfield);
|
||||||
|
ooc_release(snoopy);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run the walks in order, stopping at the first failure.
|
||||||
|
*
|
||||||
|
* Each walk takes one class through the whole life cycle and returns 0 on
|
||||||
|
* success or -1 if a step the example relies on was refused, which is enough to
|
||||||
|
* make a broken expectation visible without aborting. The short-circuit keeps the
|
||||||
|
* output of a failing run readable: no later walk can be trusted once an earlier
|
||||||
|
* one has misbehaved.
|
||||||
|
*
|
||||||
|
* The exit status is nonzero if any walk failed, so this doubles as the test the
|
||||||
|
* `test` target runs. Returns 0 when everything succeeded.
|
||||||
|
*/
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
int status = visit_dog();
|
int status = visit_dog();
|
||||||
|
|
@ -267,5 +589,14 @@ int main(void)
|
||||||
if (status == 0)
|
if (status == 0)
|
||||||
status = visit_cat();
|
status = visit_cat();
|
||||||
|
|
||||||
|
if (status == 0)
|
||||||
|
status = visit_garfield();
|
||||||
|
|
||||||
|
if (status == 0)
|
||||||
|
status = visit_snoopy();
|
||||||
|
|
||||||
|
if (status == 0)
|
||||||
|
status = visit_hierarchy();
|
||||||
|
|
||||||
return status != 0;
|
return status != 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
274
example/snoopy.c
Normal file
274
example/snoopy.c
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
/*
|
||||||
|
* This file is part of libooc.
|
||||||
|
* https://xw3.org/hanez/libooc
|
||||||
|
*
|
||||||
|
* Copyright 2026 Johannes Findeisen <you@hanez.org>
|
||||||
|
* Licensed under the terms of the Apache-2.0 license.
|
||||||
|
* https://opensource.org/license/apache-2-0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snoopy -- a subclass of Dog, and so a subclass of a subclass.
|
||||||
|
*
|
||||||
|
* The parallel of garfield.c on the other branch of the hierarchy, and the two
|
||||||
|
* together are what make the hierarchy worth having: Garfield is a Cat,
|
||||||
|
* Snoopy is a Dog, both are Animals, and a caller holding an Animal * can ask
|
||||||
|
* ooc_is_a() which one it has and get an answer.
|
||||||
|
*
|
||||||
|
* Like every subclass here, Snoopy allocates from its own class record, builds
|
||||||
|
* the classes above it by calling their constructors rather than duplicating
|
||||||
|
* them, and registers its own vtable and destructor. What this branch adds is
|
||||||
|
* the fork: `imagination` and `__flights` exist only on Snoopy, and the chain
|
||||||
|
* from Snoopy_class leads to Dog_class rather than to Cat_class, so a name
|
||||||
|
* declared on one branch is unreachable from the other.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "snoopy.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copy `text` onto the heap; the caller owns the result.
|
||||||
|
*
|
||||||
|
* A private copy of the same helper animal.c, cat.c, dog.c and garfield.c each
|
||||||
|
* keep, and identical to all of them. Four copies is past the point where a real
|
||||||
|
* codebase would publish it instead, but each class owning its own construction
|
||||||
|
* state is the arrangement the rest of these examples follow, and a divergence
|
||||||
|
* here would be the thing to notice rather than copy.
|
||||||
|
*
|
||||||
|
* Returns NULL if `text` is NULL or the allocation fails. The size is checked
|
||||||
|
* before the terminator is added, so a string long enough to wrap cannot ask
|
||||||
|
* malloc() for a short buffer.
|
||||||
|
*/
|
||||||
|
static char *dupstr(const char *text)
|
||||||
|
{
|
||||||
|
size_t len;
|
||||||
|
char *copy;
|
||||||
|
|
||||||
|
if (!text)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
len = strlen(text);
|
||||||
|
if (len == SIZE_MAX)
|
||||||
|
return NULL;
|
||||||
|
++len;
|
||||||
|
|
||||||
|
copy = malloc(len);
|
||||||
|
if (copy)
|
||||||
|
memcpy(copy, text, len);
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Virtual destructor: releases everything snoopy_new() allocated.
|
||||||
|
*
|
||||||
|
* The library calls the destructor belonging to the object's runtime type and
|
||||||
|
* stops there, so this one owns the whole hierarchy. Three frees where Dog
|
||||||
|
* needed two: `imagination` is Snoopy's, `breed` came from dog_init() and
|
||||||
|
* `name` from animal_init() before that, and neither Dog's destroy() nor
|
||||||
|
* Animal's ever runs for a Snoopy.
|
||||||
|
*
|
||||||
|
* The branch makes no difference to this. A destructor walks the struct
|
||||||
|
* downwards through whatever it was given, and the only thing that has to be
|
||||||
|
* right is that it frees each owned allocation exactly once.
|
||||||
|
*
|
||||||
|
* All three fields are marked as owned in their own class' table, so ooc_set()
|
||||||
|
* has already released a previous value where there was one and what is left is
|
||||||
|
* freed here.
|
||||||
|
*/
|
||||||
|
static void destroy(ooc_object *object)
|
||||||
|
{
|
||||||
|
Snoopy *snoopy = (Snoopy *)object;
|
||||||
|
|
||||||
|
free(snoopy->imagination);
|
||||||
|
free(snoopy->dog.breed);
|
||||||
|
free(snoopy->dog.animal.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snoopy's implementation of AnimalVTable::speak.
|
||||||
|
*
|
||||||
|
* The signature is Animal's, not Snoopy's, and the cast back down is the same
|
||||||
|
* no-op at every level, since Animal sits at offset zero however many classes
|
||||||
|
* are embedded above it.
|
||||||
|
*
|
||||||
|
* `__flights` is read directly here, from the struct definition where the member
|
||||||
|
* is in view. That is the point of a two-underscore name: it keeps callers out
|
||||||
|
* through the name-based API while leaving the class itself free to use the
|
||||||
|
* field however it likes. An accessor is what a caller would be given instead.
|
||||||
|
*
|
||||||
|
* The fallbacks keep a cleared string from reaching printf(), since a field set
|
||||||
|
* to NULL is a legitimate state and a vtable entry is called with whatever the
|
||||||
|
* object currently holds.
|
||||||
|
*/
|
||||||
|
static void speak(Animal *animal)
|
||||||
|
{
|
||||||
|
Snoopy *snoopy = (Snoopy *)animal;
|
||||||
|
|
||||||
|
printf("%s says: Woof! (%s, dreaming of %s, %d flights so far)\n",
|
||||||
|
snoopy->dog.animal.name ? snoopy->dog.animal.name : "(unnamed)",
|
||||||
|
snoopy->dog.breed ? snoopy->dog.breed : "(unknown breed)",
|
||||||
|
snoopy->imagination ? snoopy->imagination : "(nothing much)",
|
||||||
|
snoopy->__flights);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snoopy's own vtable, holding Snoopy's speak.
|
||||||
|
*
|
||||||
|
* Identical in shape to the tables in animal.c, dog.c and cat.c. Snoopy and Dog
|
||||||
|
* both declare `speak` and neither can tell the other about it: the slot is
|
||||||
|
* chosen by the class that owns the table, and a Snoopy reaches this one.
|
||||||
|
*/
|
||||||
|
static const AnimalVTable vt = {
|
||||||
|
.speak = speak,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The fields Snoopy adds on top of the ones above it.
|
||||||
|
*
|
||||||
|
* A derived class lists only what it declares itself. Everything else -- "breed"
|
||||||
|
* from Dog, then "name", "age", "_id" and "__legs" from Animal -- resolves by
|
||||||
|
* walking up from Snoopy_class through Dog_class to Animal_class.
|
||||||
|
*
|
||||||
|
* `imagination` is marked as owned, exactly as Dog's breed is, so replacing it
|
||||||
|
* through ooc_set() releases the string it held before. `__flights` carries two
|
||||||
|
* underscores, so ooc_get() withholds it as well as ooc_set() refusing to write
|
||||||
|
* it, leaving snoopy_flights() as the only route to the value.
|
||||||
|
*
|
||||||
|
* The branch is what stops a name leaking sideways: a Snoopy has no "colour"
|
||||||
|
* and a Garfield has no "imagination", because neither chain contains the class
|
||||||
|
* that would declare it.
|
||||||
|
*/
|
||||||
|
static const ooc_field Snoopy_fields[] = {
|
||||||
|
{ "imagination", offsetof(Snoopy, imagination),
|
||||||
|
sizeof(((Snoopy *)0)->imagination), 1 },
|
||||||
|
{ "__flights", offsetof(Snoopy, __flights),
|
||||||
|
sizeof(((Snoopy *)0)->__flights), 0 },
|
||||||
|
{ NULL, 0, 0, 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snoopy's runtime type record.
|
||||||
|
*
|
||||||
|
* `super` is Dog_class, and that pointer is what puts Snoopy on the Dog branch:
|
||||||
|
* ooc_new() writes this record's address into every object it allocates, and
|
||||||
|
* field lookup, ooc_set() and ooc_is_a() all start there and walk outwards,
|
||||||
|
* reaching Dog's and Animal's fields through it.
|
||||||
|
*
|
||||||
|
* `size` is sizeof(Snoopy), which must hold Dog and Snoopy's own members. The
|
||||||
|
* runtime rejects a class whose base is larger, since the base's fields would
|
||||||
|
* then be written past the end of the allocation.
|
||||||
|
*
|
||||||
|
* The record is a file-scope constant, as it is for every class.
|
||||||
|
*/
|
||||||
|
const ooc_class Snoopy_class = {
|
||||||
|
.size = sizeof(Snoopy),
|
||||||
|
.destroy = destroy,
|
||||||
|
.super = &Dog_class,
|
||||||
|
.fields = Snoopy_fields,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialise the members Snoopy owns, leaving the object ready to speak as a
|
||||||
|
* Snoopy.
|
||||||
|
*
|
||||||
|
* The third link in the chain of constructors, composed the same way
|
||||||
|
* garfield_init() is: dog_init() builds the Dog part, which calls animal_init()
|
||||||
|
* for the Animal part, and Snoopy overwrites the vtable afterwards since
|
||||||
|
* dog_init() installs Dog's table.
|
||||||
|
*
|
||||||
|
* The imagination is copied first, so a failure here cannot leave a partly built
|
||||||
|
* object behind and the caller may release the zeroed storage without the
|
||||||
|
* destructor having anything to do.
|
||||||
|
*
|
||||||
|
* `__flights` starts at zero and is written here and nowhere else, which is what
|
||||||
|
* makes the two-underscore rule meaningful: the class decides when it changes,
|
||||||
|
* and no caller can reach it by name to change it sooner.
|
||||||
|
*
|
||||||
|
* Returns 0, or -1 for a NULL object, an already initialised one, or a failed
|
||||||
|
* copy. Requires zero-initialised members and external synchronization between
|
||||||
|
* constructors in different threads.
|
||||||
|
*/
|
||||||
|
int snoopy_init(Snoopy *snoopy, const char *name, int age, const char *breed,
|
||||||
|
const char *imagination)
|
||||||
|
{
|
||||||
|
char *copy;
|
||||||
|
|
||||||
|
if (!snoopy || snoopy->imagination || snoopy->dog.breed ||
|
||||||
|
snoopy->dog.animal.vtable)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
copy = dupstr(imagination);
|
||||||
|
|
||||||
|
if (!copy)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Let Dog build the part it owns -- which lets Animal build the part it
|
||||||
|
* owns -- then take over the vtable with Snoopy's own.
|
||||||
|
*/
|
||||||
|
if (dog_init(&snoopy->dog, name, age, breed) != 0) {
|
||||||
|
free(copy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
snoopy->imagination = copy;
|
||||||
|
snoopy->dog.animal.vtable = &vt;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a Snoopy named `name`, of the given `breed` and `imagination`, and
|
||||||
|
* return it, or NULL.
|
||||||
|
*
|
||||||
|
* Two steps, as in dog_new() and cat_new(): allocate through ooc_new() with a
|
||||||
|
* reference count of one, so the caller owns the result and must release it, and
|
||||||
|
* then let snoopy_init() build the members. The allocation carries Snoopy's
|
||||||
|
* destructor from the start, which frees the whole hierarchy, so a failed init
|
||||||
|
* releases cleanly -- on every failure path above, nothing was written and
|
||||||
|
* free(NULL) is what the destructor finds.
|
||||||
|
*
|
||||||
|
* Returns NULL if the allocation fails or snoopy_init() refuses, in both cases
|
||||||
|
* leaving nothing to release.
|
||||||
|
*/
|
||||||
|
Snoopy *snoopy_new(const char *name, int age, const char *breed,
|
||||||
|
const char *imagination)
|
||||||
|
{
|
||||||
|
Snoopy *snoopy = ooc_new(&Snoopy_class);
|
||||||
|
|
||||||
|
if (!snoopy)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (snoopy_init(snoopy, name, age, breed, imagination) != 0) {
|
||||||
|
ooc_release(snoopy);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snoopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read back `__flights`, the field ooc_get() withholds.
|
||||||
|
*
|
||||||
|
* The only way to learn the value, since nothing outside the class can reach the
|
||||||
|
* field by name in either direction. Animal exposes `__legs` the same way, and
|
||||||
|
* the two differ from a single underscore only in that this one cannot be read
|
||||||
|
* by name at all.
|
||||||
|
*
|
||||||
|
* Takes a const object, so a caller with a const pointer can still be told the
|
||||||
|
* count.
|
||||||
|
*
|
||||||
|
* Returns 0 for a NULL object, which is indistinguishable from a real count of
|
||||||
|
* zero, so a caller that has to tell the two apart should check the pointer
|
||||||
|
* first.
|
||||||
|
*/
|
||||||
|
int snoopy_flights(const Snoopy *snoopy)
|
||||||
|
{
|
||||||
|
return snoopy ? snoopy->__flights : 0;
|
||||||
|
}
|
||||||
111
example/snoopy.h
Normal file
111
example/snoopy.h
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* This file is part of libooc.
|
||||||
|
* https://xw3.org/hanez/libooc
|
||||||
|
*
|
||||||
|
* Copyright 2026 Johannes Findeisen <you@hanez.org>
|
||||||
|
* Licensed under the terms of the Apache-2.0 license.
|
||||||
|
* https://opensource.org/license/apache-2-0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SNOOPY_H
|
||||||
|
#define SNOOPY_H
|
||||||
|
|
||||||
|
#include "dog.h"
|
||||||
|
|
||||||
|
typedef struct Snoopy Snoopy;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A subclass of Dog, and so a subclass of a subclass on the other branch of the
|
||||||
|
* example hierarchy.
|
||||||
|
*
|
||||||
|
* The embedded Dog comes first, which is the same arrangement Garfield uses: the
|
||||||
|
* ooc_object header stays at offset zero where the library looks for it, and the
|
||||||
|
* upcast to Animal * is a no-op cast because every address is the same. No member
|
||||||
|
* may go before the base.
|
||||||
|
*
|
||||||
|
* Snoopy adds an imagination and keeps the flying hours to itself. The important
|
||||||
|
* thing about this branch is not the members but the shape: the hierarchy now
|
||||||
|
* forks, so Garfield is a Cat and Snoopy is a Dog and neither is anything to do
|
||||||
|
* with the other beyond their shared Animal. Field lookup walks up and stops at
|
||||||
|
* the first class that has the name, so "imagination" never resolves on a
|
||||||
|
* Garfield and "colour" never resolves on a Snoopy.
|
||||||
|
*/
|
||||||
|
struct Snoopy {
|
||||||
|
Dog dog;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snoopy's own field, and the only public one it adds. Marked as owned in
|
||||||
|
* the field table, so ooc_set() releases the string it displaces exactly as
|
||||||
|
* it does for a Dog's breed. Nothing about ownership changes with depth.
|
||||||
|
*/
|
||||||
|
char *imagination;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A private counter, set once and not adjustable from outside.
|
||||||
|
*
|
||||||
|
* Two underscores rather than one, which is a difference in degree rather
|
||||||
|
* than in kind: `__flights` is unreadable as well as unwritable by name, so
|
||||||
|
* snoopy_flights() is the only way to learn it. Dog has no private field of
|
||||||
|
* its own and Animal hides `__legs` the same way, so the convention is
|
||||||
|
* visible at every level of this hierarchy.
|
||||||
|
*/
|
||||||
|
int __flights;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Snoopy's runtime type record, the value ooc_new() takes.
|
||||||
|
*
|
||||||
|
* Its `super` is Dog_class, so lookup continues from there for "breed" and
|
||||||
|
* onwards to Animal_class for "name", "age", "_id" and "__legs". Its `size` is
|
||||||
|
* sizeof(Snoopy), which has to hold Dog and Snoopy's own members on top.
|
||||||
|
*/
|
||||||
|
extern const ooc_class Snoopy_class;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a Snoopy named `name`, of the given `breed` and `imagination`, and
|
||||||
|
* return it, or NULL.
|
||||||
|
*
|
||||||
|
* The result has a reference count of one, so ownership is the caller's and it
|
||||||
|
* must reach ooc_release() eventually. Returns NULL if any step of the
|
||||||
|
* construction fails, leaving nothing to release.
|
||||||
|
*
|
||||||
|
* The object is a Snoopy and answers to Snoopy's `speak` however it is later
|
||||||
|
* reached -- through a Snoopy *, a Dog * or an Animal * alike.
|
||||||
|
*/
|
||||||
|
Snoopy *snoopy_new(const char *name, int age, const char *breed,
|
||||||
|
const char *imagination);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialise the members Snoopy owns, so a subclass of Snoopy could build this
|
||||||
|
* part for itself.
|
||||||
|
*
|
||||||
|
* The third link in the chain of constructors: snoopy_init() composes
|
||||||
|
* dog_init(), which composes animal_init(). Each one installs its own vtable and
|
||||||
|
* the one above overwrites it, so the object ends up speaking as the most
|
||||||
|
* derived class.
|
||||||
|
*
|
||||||
|
* The caller supplies storage that ooc_new() has already allocated and zeroed.
|
||||||
|
* On failure the object is left exactly as it was found, so the caller may
|
||||||
|
* release it without a destructor having anything to do.
|
||||||
|
*
|
||||||
|
* Returns 0, or -1 for a NULL object, an already initialised one, or a failed
|
||||||
|
* copy. Requires zero-initialised members and external synchronization between
|
||||||
|
* constructors in different threads.
|
||||||
|
*/
|
||||||
|
int snoopy_init(Snoopy *snoopy, const char *name, int age, const char *breed,
|
||||||
|
const char *imagination);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read back `__flights`, the field ooc_get() withholds.
|
||||||
|
*
|
||||||
|
* The only route to the value, which is what a two-underscore name asks for: no
|
||||||
|
* amount of ooc_get() or ooc_set() will reach it, so a caller wanting the count
|
||||||
|
* has to come through here. Animal exposes `__legs` the same way.
|
||||||
|
*
|
||||||
|
* Returns 0 for a NULL object, which is indistinguishable from a real count of
|
||||||
|
* zero, so a caller that has to tell the two apart should check the pointer
|
||||||
|
* first. The count starts at 0, since Snoopy has not flown anywhere yet.
|
||||||
|
*/
|
||||||
|
int snoopy_flights(const Snoopy *snoopy);
|
||||||
|
|
||||||
|
#endif /* SNOOPY_H */
|
||||||
167
tests/safety.c
167
tests/safety.c
|
|
@ -12,15 +12,18 @@
|
||||||
*
|
*
|
||||||
* Checks cover rejected metadata and arguments, overlapping field assignments,
|
* Checks cover rejected metadata and arguments, overlapping field assignments,
|
||||||
* owned-pointer replacement, reference-count limits, the example constructors,
|
* owned-pointer replacement, reference-count limits, the example constructors,
|
||||||
* and two subclasses of one base driven through a single call site. Assertions
|
* two subclasses of one base driven through a single call site, and a
|
||||||
* must be enabled: NDEBUG removes checks and API calls inside assert(), leaving
|
* three-level hierarchy whose branches do not see each other. Assertions must be
|
||||||
* an incomplete test run. Use AddressSanitizer or Valgrind as well to detect
|
* enabled: NDEBUG removes checks and API calls inside assert(), leaving an
|
||||||
|
* incomplete test run. Use AddressSanitizer or Valgrind as well to detect
|
||||||
* invalid memory accesses, invalid frees, and leaks.
|
* invalid memory accesses, invalid frees, and leaks.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <ooc/ooc.h>
|
#include <ooc/ooc.h>
|
||||||
#include "cat.h"
|
#include "cat.h"
|
||||||
#include "dog.h"
|
#include "dog.h"
|
||||||
|
#include "garfield.h"
|
||||||
|
#include "snoopy.h"
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
@ -87,6 +90,12 @@ static void check_fields(void)
|
||||||
{ "bad_owned", offsetof(struct sample, bytes), 1, 1 },
|
{ "bad_owned", offsetof(struct sample, bytes), 1, 1 },
|
||||||
{ NULL, 0, 0, 0 }
|
{ NULL, 0, 0, 0 }
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
|
* The sample class, sized for the struct and with no base. `destroy` is the
|
||||||
|
* counting destructor above, so the release at the end of this function can
|
||||||
|
* be told apart from a no-op, and the field table is the local one that mixes
|
||||||
|
* valid descriptors with invalid ones.
|
||||||
|
*/
|
||||||
const ooc_class class = { sizeof(struct sample), destroy, NULL, fields };
|
const ooc_class class = { sizeof(struct sample), destroy, NULL, fields };
|
||||||
const char *invalid[] = { "header", "past", "wrap", "huge", "empty", "bad_owned", "absent" };
|
const char *invalid[] = { "header", "past", "wrap", "huge", "empty", "bad_owned", "absent" };
|
||||||
struct sample *self = ooc_new(&class);
|
struct sample *self = ooc_new(&class);
|
||||||
|
|
@ -280,11 +289,152 @@ static void check_subclasses(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Run all four groups of checks with assertions enabled. A successful run
|
* Check a three-level hierarchy, with a subclass of a subclass on each branch.
|
||||||
* returns 0 and prints the NULL-safe speak output of both example animals. A
|
*
|
||||||
* failed assertion aborts instead of returning normally; a memory checker may
|
* The point of the depth is that lookup crosses two class records, so "colour"
|
||||||
* report additional failures. Compiling with NDEBUG disables the assertion
|
* is found at Cat_class and "name" at Animal_class, and neither is shadowed or
|
||||||
* checks.
|
* lost by Garfield sitting in between. A field declared on the other branch must
|
||||||
|
* not resolve at all: an absent name is a NULL from ooc_get() and -1 from
|
||||||
|
* ooc_set(), which is the safe answer rather than a write at a wrong offset.
|
||||||
|
*
|
||||||
|
* The underscore rules have to hold across the extra level too. `_meals` and
|
||||||
|
* `_lives` are both readable and both refuse writes, each through the class that
|
||||||
|
* declared it, while `__flights` and `__legs` are unreadable as well and only the
|
||||||
|
* accessors reach them.
|
||||||
|
*
|
||||||
|
* Replacing an owned field three levels down has to release the string that was
|
||||||
|
* there before, and the destructor then has three allocations to free rather than
|
||||||
|
* one. Only a memory checker can see whether the middle one was freed: a missing
|
||||||
|
* free at this depth is silent, since the object still works perfectly well.
|
||||||
|
* That is what the last release below is checking.
|
||||||
|
*
|
||||||
|
* Both objects are also spoken through one Animal * each, so the printed lines
|
||||||
|
* are the evidence that a three-level override reaches its own implementation.
|
||||||
|
* Repeated initialisation is refused at every level, leaving the existing members
|
||||||
|
* intact, and the constructor checks reject a NULL argument at each step: the
|
||||||
|
* food and the imagination fail last, and the colour and the breed fail inside the
|
||||||
|
* base part, which is a different cleanup path again.
|
||||||
|
*/
|
||||||
|
static void check_deep_hierarchy(void)
|
||||||
|
{
|
||||||
|
Garfield *garfield = garfield_new("Garfield", 4, "orange", "lasagna");
|
||||||
|
Snoopy *snoopy = snoopy_new("Snoopy", 3, "beagle", "his red baron");
|
||||||
|
Animal *garfield_view;
|
||||||
|
Animal *snoopy_view;
|
||||||
|
char *food = malloc(sizeof("pizza"));
|
||||||
|
char *dream = malloc(sizeof("a nap"));
|
||||||
|
char *empty = NULL;
|
||||||
|
int value = 7;
|
||||||
|
|
||||||
|
assert(garfield && snoopy && food && dream);
|
||||||
|
memcpy(food, "pizza", sizeof("pizza"));
|
||||||
|
memcpy(dream, "a nap", sizeof("a nap"));
|
||||||
|
|
||||||
|
/* Each level's own field, and one found two records up. */
|
||||||
|
assert(ooc_get(garfield, "favourite_food") == &garfield->favourite_food);
|
||||||
|
assert(ooc_get(garfield, "colour") == &garfield->cat.colour);
|
||||||
|
assert(ooc_get(garfield, "name") == &garfield->cat.animal.name);
|
||||||
|
assert(ooc_get(snoopy, "imagination") == &snoopy->imagination);
|
||||||
|
assert(ooc_get(snoopy, "breed") == &snoopy->dog.breed);
|
||||||
|
assert(ooc_get(snoopy, "name") == &snoopy->dog.animal.name);
|
||||||
|
|
||||||
|
/* The other branch's field is absent from this chain, not misread. */
|
||||||
|
assert(ooc_get(garfield, "imagination") == NULL);
|
||||||
|
assert(ooc_set(garfield, "imagination", dream) == -1);
|
||||||
|
assert(ooc_get(snoopy, "colour") == NULL);
|
||||||
|
assert(ooc_set(snoopy, "colour", dream) == -1);
|
||||||
|
assert(ooc_get(snoopy, "favourite_food") == NULL);
|
||||||
|
assert(ooc_get(garfield, "breed") == NULL);
|
||||||
|
|
||||||
|
/* Private in the declaring class, readable and unwritable at any depth. */
|
||||||
|
assert(*(int *)ooc_get(garfield, "_meals") == 1);
|
||||||
|
assert(ooc_set(garfield, "_meals", &value) == -1);
|
||||||
|
assert(*(int *)ooc_get(garfield, "_meals") == 1);
|
||||||
|
assert(ooc_set(garfield, "_lives", &value) == -1);
|
||||||
|
assert(*(int *)ooc_get(garfield, "_lives") == 9);
|
||||||
|
|
||||||
|
/* Two underscores withhold the field from ooc_get() as well. */
|
||||||
|
assert(ooc_get(garfield, "__legs") == NULL);
|
||||||
|
assert(ooc_get(snoopy, "__flights") == NULL);
|
||||||
|
assert(ooc_set(snoopy, "__flights", &value) == -1);
|
||||||
|
assert(snoopy_flights(snoopy) == 0);
|
||||||
|
assert(garfield_meals(garfield) == 1);
|
||||||
|
assert(garfield_meals(NULL) == 0);
|
||||||
|
assert(snoopy_flights(NULL) == 0);
|
||||||
|
|
||||||
|
/* Owned replacement on the deepest own field, and on an inherited one. */
|
||||||
|
assert(ooc_set(garfield, "favourite_food", &food) == 0);
|
||||||
|
assert(strcmp(garfield->favourite_food, "pizza") == 0);
|
||||||
|
assert(ooc_set(snoopy, "imagination", &dream) == 0);
|
||||||
|
assert(strcmp(snoopy->imagination, "a nap") == 0);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Subtype tests walk the whole chain, so a three-level object answers for
|
||||||
|
* every ancestor, while the sibling branch stays unrelated at any depth.
|
||||||
|
*/
|
||||||
|
assert(ooc_is_a(garfield, &Garfield_class));
|
||||||
|
assert(ooc_is_a(garfield, &Cat_class));
|
||||||
|
assert(ooc_is_a(garfield, &Animal_class));
|
||||||
|
assert(!ooc_is_a(garfield, &Dog_class));
|
||||||
|
assert(!ooc_is_a(garfield, &Snoopy_class));
|
||||||
|
assert(ooc_is_a(snoopy, &Snoopy_class));
|
||||||
|
assert(ooc_is_a(snoopy, &Dog_class));
|
||||||
|
assert(ooc_is_a(snoopy, &Animal_class));
|
||||||
|
assert(!ooc_is_a(snoopy, &Cat_class));
|
||||||
|
assert(!ooc_is_a(snoopy, &Garfield_class));
|
||||||
|
|
||||||
|
/* The exact type is the deepest record, with no walk involved. */
|
||||||
|
assert(garfield->cat.animal.object.class == &Garfield_class);
|
||||||
|
assert(snoopy->dog.animal.object.class == &Snoopy_class);
|
||||||
|
|
||||||
|
/* A three-level override, reached through one Animal pointer each. */
|
||||||
|
garfield_view = ooc_retain(&garfield->cat.animal);
|
||||||
|
snoopy_view = ooc_retain(&snoopy->dog.animal);
|
||||||
|
assert(garfield_view && snoopy_view);
|
||||||
|
assert(garfield_view->vtable->speak != snoopy_view->vtable->speak);
|
||||||
|
animal_speak(garfield_view);
|
||||||
|
animal_speak(snoopy_view);
|
||||||
|
|
||||||
|
/* Repeated initialisation is refused at every level, members intact. */
|
||||||
|
assert(garfield_init(garfield, "again", 1, "red", "pizza") == -1);
|
||||||
|
assert(strcmp(garfield->cat.animal.name, "Garfield") == 0);
|
||||||
|
assert(snoopy_init(snoopy, "again", 1, "poodle", "food") == -1);
|
||||||
|
assert(strcmp(snoopy->dog.animal.name, "Snoopy") == 0);
|
||||||
|
assert(garfield_init(NULL, "name", 0, "colour", "food") == -1);
|
||||||
|
assert(snoopy_init(NULL, "name", 0, "breed", "dream") == -1);
|
||||||
|
assert(cat_init(NULL, "name", 0, "colour") == -1);
|
||||||
|
assert(dog_init(NULL, "name", 0, "breed") == -1);
|
||||||
|
assert(cat_init(&garfield->cat, "again", 1, "red") == -1);
|
||||||
|
assert(dog_init(&snoopy->dog, "again", 1, "poodle") == -1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cleared strings must not reach printf(), and the destructor then frees what
|
||||||
|
* is left: three allocations here, so a memory checker is the only thing that
|
||||||
|
* can catch a missing free at the middle level.
|
||||||
|
*/
|
||||||
|
assert(ooc_set(garfield, "favourite_food", &empty) == 0);
|
||||||
|
assert(ooc_set(garfield, "colour", &empty) == 0);
|
||||||
|
assert(ooc_set(snoopy, "imagination", &empty) == 0);
|
||||||
|
assert(ooc_set(snoopy, "breed", &empty) == 0);
|
||||||
|
animal_speak(garfield_view);
|
||||||
|
ooc_release(garfield_view);
|
||||||
|
ooc_release(garfield);
|
||||||
|
ooc_release(snoopy_view);
|
||||||
|
ooc_release(snoopy);
|
||||||
|
|
||||||
|
/* NULL arguments at each constructor step, from both branches. */
|
||||||
|
assert(garfield_new("name", 0, NULL, "food") == NULL);
|
||||||
|
assert(garfield_new("name", 0, "colour", NULL) == NULL);
|
||||||
|
assert(snoopy_new("name", 0, NULL, "dream") == NULL);
|
||||||
|
assert(snoopy_new("name", 0, "breed", NULL) == NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all five groups of checks with assertions enabled. A successful run
|
||||||
|
* returns 0 and prints the speak output of the example animals, including the
|
||||||
|
* NULL-safe fallbacks. A failed assertion aborts instead of returning normally; a
|
||||||
|
* memory checker may report additional failures. Compiling with NDEBUG disables
|
||||||
|
* the assertion checks.
|
||||||
*/
|
*/
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
|
|
@ -292,5 +442,6 @@ int main(void)
|
||||||
check_classes();
|
check_classes();
|
||||||
check_example();
|
check_example();
|
||||||
check_subclasses();
|
check_subclasses();
|
||||||
|
check_deep_hierarchy();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue