Wednesday, July 20, 2022

C++ API Design Best Practices - Function Design


Function Design Options

There are many interface options you can control when designing a function call, for free functions you should consider the following alternatives

  • Static versus non-static function.
  • Pass arguments by value, reference, or pointer.
  • Pass arguments as const or non-const.
  • Use of optional arguments with default values.
  • Return the result by value, reference, or pointer.
  • Return the result as const or non-const.
  • Operator or non-operator function.
  • Use of exception specifications

considers all of these free function options as well as the
following:

  • Virtual versus non-virtual member function.
  • Pure virtual versus non-pure virtual member function.
  • Const versus non-const member function.
  • Public, protected, or private member function.
  • Use of the explicit keyword for non-default constructors

 Function Naming

  • Functions used to set or return some value should fully describe that quantity using standard prefixes such as Get and Set
  • Functions that answer yes or no queries should use an appropriate prefix to indicate this behavior, such as Is, Are, or Has, and should return a bool result, for example, IsEnabled(), ArePerpendicular(), or HasChildren(). empty() instead of IsEmpty().
  • Functions used to perform some action should be named with a strong verb, for example, Enable (), Print(), or Save().FUNCTION DESIGN
  • Use positive concepts to name your functions rather than framing them in the negative. For example, use the name IsConnected() instead of IsUnconnected(). This can help to avoid user confusion when faced with double negatives like !IsUnconnected(). Function names should describe everything that the routine
  • Function names should describe everything that the routine does. For example, if a routine in an image processing library performs a sharpening filter on an image and saves it to disk, the method should be called something like SharpenAndSaveImage() instead of just SharpenImage(). If this makes your function names too long, then this may indicate that they are performing too many tasks and should be split up.
  • You should avoid abbreviations. Names should be self-explanatory and memorable, but the use of abbreviations can introduce confusing or obscure terminology. For example, the user has to remember if you are using GetCurrentValue(), GetCurrValue(), GetCurValue(), or GetCurVal().
  • Functions should not begin with an underscore character (_). The C++ standard states that global symbols starting with an underscore are reserved for internal compiler use. The same is true for all symbols that begin with two underscores followed by a capital letter.
  • Functions that form natural pairs should use the correct complementary terminology. For example, OpenWindow() should be paired with CloseWindow(), not DismissWindow(). The use of precise opposite terms makes it clearer to the user that one function performs the opposite function of another function

Function Parameters

  • Use meaningful arguments

                    string setName(const string str1, const string str2);
                    and
                    string setName(const string firstName, const string surName);

  • The second signature gives a much better indication of how to use the function simply through the use of descriptive parameter names.

Reducing  parameter list 

  • Avoid long parameter lists.
  • For functions that accept many optional parameters, you may consider passing the arguments using a struct or map instead. For example,
                    struct OpenWindowParams
                    {
                        OpenWindowParams();
                        int mX;
                        int mY;
                        int mWidth;
                        int mHeight;
                        int mFlags;
                        std::string mClassName;
                        std::string mWindowName;
                    };

                    void OpenWindow(const OpenWindowParams &params);
  • This technique is also a good way to deal with argument lists that may change over the life of the API. A newer version of the API can simply add new fields to the end of the structure without changing the signature of the OpenWindow() function. You can also add a version field (set by the constructor) to allow binary compatible changes to the structure: the OpenWindow() function can then check the version field to determine what information is included in the structure.
  • Values can be specified in any order because function calls are order-independent.
  • The purpose of each value is more evident because you must use a named function to set the value, for example, setInterval().
  • Optional parameters are supported by simply not calling the appropriate function.
  • The constructor can define reasonable default values for all settings.
  • Adding new parameters is backward compatible because no existing functions need to change the signature. Only new functions are added. Taking this even further, we could make each of

Error Handling

  • Use a consistent and well-documented error-handling mechanism.
  • Three  main ways of dealing with error conditions in your API are

    1. Returning error codes.
    2. Throwing exceptions.
    3. Aborting the program.

  • Handling an exception can be an expensive operation due to the run-time stack unwinding behavior. Also, an uncaught exception can cause your clients’ programs to abort, resulting in data loss and frustration for their end users. 
  • If you do opt to use exceptions to signal unexpected situations in your code, here are some best practices to observe.
  • Derive your own exceptions from std::exception and define a what() method to describe the failure.

  • Consider using RAII techniques to maintain exception safety, that is, to ensure that resources get cleaned up correctly when an exception is thrown
  • Make sure that you document all of the exceptions that can be thrown by a function in its comments.
  • You might be tempted to use exception specifications to document the exceptions that a function may throw
  • Create exceptions for the set of logical errors that can be encountered, not a unique exception for every individual physical error that you raise.
  • If you handle exceptions in your own code, then you should catch the exception by reference (as in the aforementioned example) to avoid calling the copy constructor for the thrown object. Also, try to avoid the catch(. . .) syntax because some compilers also throw an exception when a programming error arises, such as an assert() or segmentation fault.
  • If you have an exception that multiplies inherits from more than one base exception class, you should use virtual inheritance to avoid ambiguities and subtle errors in your client’s code where they attempt to catch your exceptions. 
  • Derive your own exceptions from std::exception.
  • Also, any error code or exception description should represent the actual failure. Invent a new error code or exception if existing ones do not describe the error accurately. You will infuriate your users if they waste time trying to debug the wrong problem because your error reporting was inaccurate or plain wrong

References 

  • API Design for C++ Book by Martin Reddy

No comments:

Post a Comment

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...