So I have started updating myself with Modern C++ a while ago & since my post 21 new features of Modern C++ to use in your project & All about lambda function in C++ was popular I decided to write about advance C++ concepts & idioms which I have learned from this wikibook & course.
There are many other advance C++ concepts & idioms as well but I consider these 7 as “should-know”. To explain them, I have taken a more pragmatic approach than sophistication. So, I have weighed more on readability, simplicity over other fancy features, syntax sugars and complexity.
Note: There are also drawbacks of using some of these techniques which I have not discussed here or maybe I am not qualified enough.
1. RAII
Intent: To guarantee the release of resource(s) at the end of a scope.
Implementation: Wrap resource into a class; resource acquired in the constructor immediately after its allocation; and automatically released in the destructor; resource used via an interface of the class;
Also known as: Execute-around object, Resource release is finalization, Scope-bound resource management
Problem
- Resource Acquisition Is Initialization idiom is the most powerful & vastly used idiom although the name is really terrible as the idiom is rather about resource release than acquisition.
- RAII guarantee the release of resource at the end of a scope/destruction. It thus ensures no resource leaks and provides basic exception safety guarantee.
|
|
- In the above code, the early
return
orthrow
statement, causing the function to terminate withoutptr
being deleted. - In addition, the memory allocated for variable
ptr
is now leaked (and leaked again every time this function is called and returns early).
Solution
|
|
- Note that no matter what happens after
ptr
declaration,ptr
will be destroyed when the function terminates (regardless of how it terminates). - As the
ptr
is a local object, the destructor will be called while the function stack frame rewinds. Hence, we are assured that theresource
will be properly cleaned up.
Usecases
- Using RAII, resources like
new
/delete
,malloc
/free
, acquire/release, mutex lock/unlock, file open/close, count++
/--
, database connect/disconnect or anything else that exists in limited supply can easily be managed. - Examples from C++ Standard Library include
std::unique_ptr
,std::ofstream
,std::lock_guard
, etc.
2. Return Type Resolver
Intent: To deduce the type of the object being initialize or assign to.
Implementation: Uses templatized conversion operator.
Also known as: Return type overloading
Issue
- A function can not overloaded only by its return type.
Solution
|
|
If you are unaware of constexpr, I have written a short post on when to use const vs constexpr in c++.
Usecases
- So, when you use
nullptr
(introduced in C++11), this is the technique that runs under the hood to deduce the correct type depending upon the pointer variable it is assigning to. - You can also overcome the function overloading limitation on the basis of a return type as we have seen above.
- Return Type Resolver can also used to provide a generic interface for assignment, independent of the object assigned to.
3. Type Erasure
Intent: To create generic container that can handle a variety of concrete types.
Implementation: Can be implemented by void*
, templates, polymorphism, union, proxy class, etc.
Also known as: Duck-typing
Problem
- C++ is a statically typed language with strong typing. In statically typed languages, object type known & set at compile-time. While in dynamically typed languages the type associated with run-time values.
- In other words, in strongly typed languages the type of an object doesn’t change after compilation.
- However, to overcome this limitation & providing a feature like dynamically typed languages, library designers come up with various generic container kind of things like
std::any
(C++17),std::variant
(C++17),std::function
(C++11), etc.
Different Type Erasure Techniques
- There is no one strict rule on how to implement this idiom, it can have various forms with its own drawbacks as follows:
=> Type erasure using void* (like in C)
Drawback: not safe & separate compare function needed for each type
=> Type erasure using C++ templates
Drawback: may lead to many function template instantiations & longer compilation time
=> Type erasure using polymorphism
|
|
Drawback: run-time cost (dynamic dispatch, indirection, vtable, etc.)
=> Type erasure using union
Drawback: not type-safe
Solution
- As I mentioned earlier that standard library already has such generic containers.
- To understand type erasure better let’s implement one i.e.
std::any
:
|
|
Especially, the thing here to note is how we are leveraging empty static method i.e. inner<T>::type()
to determine template instance type in any_cast<T>
.
Usecases
- Employ to handle multiple types of the return value from function/method(Although that’s not recommended advice).
4. CRTP
Intent: To achieve static polymorphism.
Implementation: Make use of base class template spcialization.
Also known as: Upside-down inheritance, Static polymorphism
Problem
|
|
- For each comparable objects, you need to define respective comparison operators. This is redundant because if we have an
operator <
, we can overload other operators on the basis of it. - Thus,
operator <
is the only one operator having type information, other operators can be made type independent for reusability purpose.
Solution
- Curiously Recurring Template Pattern implementation rule is simple, separate out the type-dependent & independent functionality and bind type independent functionality with the base class using self-referencing template.
- Above line may seem cryptic at first. So, consider the below solution to the above problem for more clarity:
|
|
Usecases
- CRTP widely employed for static polymorphism without bearing the cost of virtual dispatch mechanism. Consider the following code we have not used virtual keyword & still achieved the functionality of polymorphism(specifically static polymorphism).
|
|
- CRTP can also used for optimization as we have seen above it also enables code reusability.
Update: The above hiccup of declaring multiple comparisons operator will permanently be sorted from C++20 by using spaceship(<=>
)/Three-way-comparison operator.
5. Virtual Constructor
Intent: To create a copy or new object without knowing its concrete type.
Implementation: Exploits overloaded methods with polymorphic assignment.
Also known as: Factory method/design-pattern.
Problem
- C++ has the support of polymorphic object destruction using it’s base class’s virtual destructor. But, equivalent support for creation and copying of objects is missing as С++ doesn’t support virtual constructor, copy constructors.
- Moreover, you can’t create an object unless you know its static type, because the compiler must know the amount of space it needs to allocate. For the same reason, copy of an object also requires its type to known at compile-time.
|
|
Solution
- The Virtual Constructor technique allows polymorphic creation & copying of objects in C++ by delegating the act of creation & copying the object to the derived class through the use of virtual methods.
- Following code is not only implement virtual constructor(i.e.
create()
) but also implements virtual copy constructor (i.e.clone()
) .
|
|
Usecases
- To provide a generic interface to produce/copy a variety of classes using only one class.
6. SFINAE and std::enable_if
Intent: To filter out functions that do not yield valid template instantiations from a set of overloaded functions.
Implementation: Achieved automatically by compiler or exploited using std::enable_if.
Also known as:
Motivation
- Substitution Failure Is Not An Error is a language feature(not an idiom) a C++ compiler uses to filter out some templated function overloads during overload resolution.
- During overload resolution of function templates, when substituting the explicitly specified or deduced type for the template parameter fails, the specialization discarded from the overload set instead of causing a compile error.
- Substitution failure happens when type or expression ill-formed.
- Imagine if you want to create two sets(based on primitive type & user-defined type separately) of a function having the same signature?
Solution
|
|
- Above code snippet is a short example of exploiting SFINAE using
std::enable_if
, in which first template instantiation will become equivalent tovoid func<(anonymous), void>((anonymous) * t) and second,
void func(int * t). - You can read more about
std::enable_if
here.
Usecases
- Together with
std::enable_if
, SFINAE is heavily used in template metaprogramming. - The standard library also leveraged SFINAE in most type_traits utilities. Consider the following example which checks for the user-defined type or primitive type:
|
|
- Without SFINAE, you would get a compiler error, something like “
0
cannot be converted to member pointer for a non-class typeint
” as both the overload oftest
method only differs in terms of the return type. - Because
int
is not a class, so it can’t have a member pointer of typeint int::*
.
7. Proxy
Intent: To achieve intuitive functionality using middleware class.
Implementation: By use of temporary/proxy class.
Also known as: operator []
(i.e. subscript) proxy, double/twice operator overloading
Motivation
- Most of the dev believes this is only about the subscript operator (i.e.
operator[ ]
), but I believe type/class that comes in between exchanging data is proxy. - We have already seen a nice example of this idiom indirectly above in type-erasure(i.e. class
any::inner<>
). But still, I think one more example will add concreteness to our understanding.
operator [ ] solution
|
|
Usecases
- To create intuitive features like double operator overloading,
std::any
etc.
Summary by FAQs
When to actually use RAII?
When you have set of steps to carry out a task & two steps are ideal i.e. set-up & clean-up, then that’s the place you can employ RAII.
Why can’t functions be overloaded by return type?
You can’t overload on return types as it is not mandatory to use the return value of the functions in a function call expression. For example, I can just say
get_val()
;`
What does the compiler do now?
When to use return type resolver idiom?
You can apply return type resolver idiom when your input types are fixed but output types may vary.
What is type erasure in C++?
- Type erasure technique is used to design generic type which relies on the type of assignment(as we do in python).
- By the way, do you know
auto
or can you design one now?
Best scenarios to apply type erasure idiom?
- Useful in generic programming.
- Can also be used to handle multiple types of the return value from function/method(Although that’s not recommended advice).
What is the curiously recurring template pattern (CRTP)?
CRTP is when a class A
has a base class. And that base class is a template specialization for the class A
itself. E.g.template <class T>
class X{...};
class A : public X<A> {...};
It is curiously recurring, isn’t it?
Why Curiously Recurring Template Pattern (CRTP) works?
I think this answer is very appropriate.
What is SFINAE?
Substitution Failure Is Not An Error is a language feature(not an idiom) a C++ compiler uses to filter out some templated function overloads during overload resolution.
What is Proxy Class in C++?
A proxy is a class that provides a modified interface to another class.
Why do we not have a virtual constructor in C++?
- A virtual-table(vtable) is made for each Class having one or more ‘virtual-functions’. Whenever an object is created of such class, it contains a ‘virtual-pointer’ which points to the base of the corresponding vtable. Whenever there is a virtual function call, the vtable is used to resolve to the function address.
- A constructor can not be virtual, because when the constructor of a class is executed there is no vtable in the memory, means no virtual pointer defined yet. Hence the constructor should always be non-virtual.
Can we make a class copy constructor virtual in C++?
Similar to “Why do we not have a virtual constructor in C++?” which already answered above.
What are the use cases & need for virtual constructor?
To create & copy the object(without knowing its concrete type) using a base class polymorphic method.