why handle to an object frequently appears as pointer-to-pointer

Viewed 126

What is the intention to set handle to an object as pointer-to pointer but not pointer? Like following code:

FT_Library library;
FT_Error error = FT_Init_FreeType( &library );

where

typedef struct FT_LibraryRec_  *FT_Library

so &library is a FT_LIBraryRec_ handle of type FT_LIBraryRec_**

3 Answers

It's a way to emulate pass by reference in C, which otherwise only have pass by value.

The 'C' library function FT_Init_FreeType has two outputs, the error code and/or the library handle (which is a pointer).

In C++ we'd more naturally either:

  • return an object which encapsulated the success or failure of the call and the library handle, or

  • return one output - the library handle, and throw an exception on failure.

C APIs are generally not implemented this way.

It is not unusual for a C Library function to return a success code, and to be passed the addresses of in/out variables to be conditionally mutated, as per the case above.

The approach hides implementation. It speeds up compilation of your code. It allows to upgrade data structures used by the library without breaking existing code that uses them. Finally, it makes sure the address of that object never changes, and that you don’t copy these objects.

Here’s how the version with a single pointer might be implemented:

struct FT_Struct
{
    // Some fields/properties go here, e.g.
    int field1;
    char* field2;
}
FT_Error Init( FT_Struct* p )
{
    p->field1 = 11;
    p->field2 = malloc( 100 );
    if( nullptr == p->field2 )
        return E_OUTOFMEMORY;
    return S_OK;
}

Or C++ equivalent, without any pointers:

class FT_Struct
{
    int field1;
    std::vector<char> field2;
public:
    FT_Struct() :
        field1( 11 )
    {
        field2.resize( 100 );
    }
};
  1. As a user of the library, you have to include struct/class FT_Struct definition. Libraries can be very complex so this will slow down compilation of your code.
  2. If the library is dynamic i.e. *.dll on windows, *.so on linux or *.dylib on osx, you upgrade the library and if the new version changes memory layout of the struct/class, old applications will crash.
  3. Because of the way C++ works, objects are passed by value, i.e. you normally expect them to be movable and copiable, which is not necessarily what library author wants to support.

Now consider the following function instead:

FT_Error Init( FT_Struct** pp )
{
    try
    {
        *pp = new FT_Struct();
        return S_OK;
    }
    catch( std::exception& ex )
    {
        return E_FAIL;
    }
}

As a user of the library, you no longer need to know what’s inside FT_Struct or even what size it is. You don’t need to #include the implementation details, i.e. compilation will be faster. This plays nicely with dynamic libraries, library author can change memory layout however they please, as long as the C API is stable, old apps will continue to work. The API guarantees you won’t copy or move the values, you can’t copy structures of unknown lengths.

Related