What is an opaque value in C++?

“Opaque” is defined, in English, as “not able to be seen through; not transparent”. In Computer Science, this means a value which reveals no details other then the type of the value itself.

People often use the C type FILE as the classic example but often this is not opaque – the details are revealed in stdio.h for anyone to see and they simply rely on the user of the type to not fiddle with the internals. That’s fine as long as people stick to the rules, only passing such values to functions like fread() and fclose() but the problem with revealing information is that people sometimes (foolishly) begin to rely on it.

For example, glibc publishes its FILE structure (as struct _IO_FILE) in libio.h so that type is not technically opaque.

Note that part of the definition at the front: “not able” rather than “not willing”. Opacity requires the information to be hidden rather than just enacting a “gentleman’s agreement” not to use it.

Opaque pointers, done correctly, should reveal no information other than the type name itself and you can implement that in C relatively easily. Consider the following header file prog2.h for obtaining and releasing xyzzy objects:

struct xyzzy;
struct xyzzy *xyzzyOpen (void);
void xyzzyClose (struct xyzzy *fh);

This is all that clients of the code see, an incomplete type struct xyzzy and some functions to allocate and release objects of that type (they don’t get to see prog2.c detailed below). Note that pointers to an incomplete type are fine but you cannot instantiate an object of that type since you don’t know its internals. So the code:

struct xyzzy myvar;

would cause an error along the lines of:

prog1.c: In function ‘main’:
prog1.c:3:15: error: storage size of 'myvar' isn't known

Now you can quite happily use those functions from a program prog1.c without knowing the internals of the structure:

#include "prog2.h"
int main (void) {
    //struct xyzzy myvar;             // will error
    struct xyzzy *num1 = xyzzyOpen();
    struct xyzzy *num2 = xyzzyOpen();
    struct xyzzy *num3 = xyzzyOpen();
    xyzzyClose (num1);
    xyzzyClose (num3);                // these two intentionally
    xyzzyClose (num2);                //   reversed.
    return 0;
}

And the implementation of the calls, prog2.c, actually controls and knows the internals, so can use them quite freely:

#include <stdio.h>
#include <stdlib.h>
#include "prog2.h"

struct xyzzy { int payload; };
static int payloadVal = 42;

struct xyzzy *xyzzyOpen (void) {
    struct xyzzy *plugh = malloc (sizeof (struct xyzzy));
    plugh->payload = payloadVal++;
    printf ("xyzzyOpen payload = %d\n", plugh->payload);
    return plugh;
}

void xyzzyClose (struct xyzzy *plugh) {
    printf ("xyzzyClose payload = %d\n", plugh->payload);
    free (plugh);
}

The printf calls are there simply to show that it can use the internals, and you’d probably want to add checking of the return value from malloc in production-ready code but that’s not relevant to the purpose of this example.

When you compile prog1.c and prog2.c into a single executable and run it, the output is:

xyzzyOpen payload = 42
xyzzyOpen payload = 43
xyzzyOpen payload = 44
xyzzyClose payload = 42
xyzzyClose payload = 44
xyzzyClose payload = 43

as you would expect from the main function.

Leave a Comment