Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 6.  Scoping Classes


6.3. APIs and Services

6.3.1 APIs

As we'll see in Chapter 11, the only reliably portable way to use a C-API is to call its initialization function before using any of its facilities, and then to call the uninitialization function after you have finished with it. This has the following form:



int main(. . .)


{


  Acme_Init(); // May or may not fail. Must test if it can!


  . . . // The main processing of the application, using Acme API


  Acme_Uninit();


}



This is a clear example of the need for RAII. Such APIs often have a reference-counted implementation, such that Acme_Uninit() must be called once for each call to Acme_Init(). It is only at that point that the API ensures that any API-specific resources are released. If the call count is not balanced, then the release will not happen, and the result will be anything from certain resource leaks to possible process failure.

When dealing with such C-APIs from C++, this balance can usually be achieved in a really simple way. Where the API initialization function does not have a failure condition, it is trivial, as shown in Listing 6.12.

Listing 6.12.


#ifdef _ _cplusplus


extern "C" {


#endif /* _ _cplusplus */





 void Acme_Init(void);


 void Acme_Uninit(void);





#ifdef _ _cplusplus


} /* extern "C" */





class Acme_API_Scope


{


public:


  Acme_API_Scope()


  {


    Acme_Init();


  }


  ~Acme_API_Scope()


  {


    Acme_Uninit();


  }


// Not to be implemented


  . . .


};





#endif /* _ _cplusplus */



It's not quite so simple when the initialization function can fail, as is most often the case. Because some APIs may need to be initialized before other services, including those that support C++ features, it is not always appropriate to throw exceptions. Conversely, an API failure is indeed an exceptional circumstance [Kern1999], and where supportable it seems more appropriate to have an exception thrown. You can either implement a testable scoping class that implements operator bool() const and operator !() const or equivalent operators (see chapter 24), or always throws a specific exception. We'll look at the trade-offs between these two approaches now.

If the Acme API initializing function could fail, then you may choose to code the scoping class as shown in Listing 6.13:

Listing 6.13.


int Acme_Init(void); // Returns 0 if succeeded, non-0 otherwise





class Acme_API_Scope


{


public:


  Acme_API_Scope()


    : m_bInitialised(0 == Acme_Init())


  {}


  ~Acme_API_Scope()


  {


    if(m_bInitialised)


    {


      Acme_Uninit();


    }


  }


public:


  operator bool () const


  {


    return m_bInitialised;


  }


private:


  bool m_bInitialised;


};



and use it in the following way:



int main(. . .)


{


  Acme_API_Scope  acme_scope;





  if(acme_scope)


  {


    . . . // Do the main business of the program


  }



This is fine, but it's not hard to imagine how painful it can become when there are many different APIs to initialize. Checking each and every scoping instance leads to a lack of detailed error information:



{


  scope_1_t scope_1(. . .);


  scope_2_t scope_2(. . .);


  scope_3_t scope_3(. . .);


  scope_4_t scope_4(. . .);





  if( !scope_1 ||


      !scope_2 ||


      !scope_3 ||


      !scope_4)


  {


    . . . // only a generic "something failed"



or tedious and verbose code:



{


  scope_1_t scope_1(. . .);


  scope_3_t scope_3(. . .);


  scope_4_t scope_4(. . .);





  if(!scope_1)


  {


    . . . // log/signal error, or terminate, or whatever


  }


  else


  {


    scope_3_t scope_3(. . .);





    if(!scope_2)


    . . . // log/signal error, or terminate, or whatever


    }


    else


    {


      . . . // ad infinitum



This can get seriously tiresome, and is close to the canonical form of "good reason to use exceptions." In these circumstances it is much easier to use throwing exception-scoping initializer classes, with the following client-code form:



int main(. . .)


{


  try


  {


    scope_1_t scope_1(. . .);


    scope_2_t scope_2(. . .);


    scope_3_t scope_3(. . .);


    scope_4_t scope_4(. . .);





    . . . // Do the main business of the program


  }


  catch(std::exception &x)


  {


    . . . // Do something with the exception


  }





  . . .



However, some APIs have to be set up independently of exception-handling mechanisms, so the given form isn't always achievable and/or desirable. Hence, the third, and probably best, option is to use a policy-based approach, which we'll see in section 19.8.

Whichever way you (have to) play it, using scoping classes for keeping APIs alive for the period that you need increases safety and reduces coding. Do it!

6.3.2 Services

As well as scoping API active lifetimes, scoping classes can be used to temporarily alter API states. For example, Microsoft's C run time library has a debugging API, which can be tuned using the _CrtSetDbgFlag() function. Sometimes it is useful to alter the debugging information that the library is providing for a short period, and again this is classic scoping class territory:



{


  . . .


  { // Suspend block release to stress memory system


    CrtDbgScope      scope(_CRTDBG_DELAY_FREE_MEM_DF, 0);


    . . . // Stressful section of code


*  } // return to normal




      Previous section   Next section