My journey to master Modern C++ began some time ago, and following the success of my previous posts, “21 new features of Modern C++ to use in your project” and “All about lambda function in C++“, I decided to explore advanced C++ concepts and idioms that I’ve learned from this wikibook and course, which ultimately led me to unlock C++ mastery.
While there are numerous advanced C++ concepts and idioms beyond those listed in this article, I consider these 7 to be essential knowledge for any serious programmer. To explain them, I’ve adopted a pragmatic approach, prioritizing clarity and simplicity over elaborate features, syntactic sugar, and complexity. For instance, you can find more expert-level concepts at this resource.
Note: It’s worth noting that some of these techniques also have drawbacks, which I haven’t discussed here, possibly due to my limited expertise.
1. RAII
Intent: To ensure the release of resource(s) at the end of a scope, thereby preventing resource leaks and providing a basic exception safety guarantee.
Implementation: Encapsulate a resource within a class; acquire the resource in the constructor immediately after allocation; and automatically release it in the destructor; access the resource via the class interface.
Also known as: Execute-around object, Resource release is finalization, Scope-bound resource management.
Problem
- Resource Acquisition Is Initialization idiom is a powerful and widely used concept, despite its misleading name, as it’s more about resource release than acquisition.
- RAII guarantees the release of resources at the end of a scope/destruction, thereby preventing resource leaks and providing a basic exception safety guarantee.
struct resource
{
resource(int x, int y) { cout << "resource acquired\n"; }
~resource() { cout << "resource destroyed\n"; }
};
void func()
{
resource *ptr = new resource(1, 2);
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
throw 0; // the function returns early, and ptr won't be deleted!
if (x < 0)
return; // the function returns early, and ptr won't be deleted!
// do stuff with ptr here
delete ptr;
}
- In the above code, the early
return
orthrow
statement, causes the function to terminate withoutptr
being deleted. - Consequently, the memory allocated for the variable,
ptr
, is now leaked (and leaked again every time this function is called and returns early).
Solution
template
class smart_ptr
{
T* m_ptr;
public:
template
smart_ptr(Args&&... args) : m_ptr(new T(std::forward(args)...)){}
~smart_ptr() { delete m_ptr; }
smart_ptr(const smart_ptr& rhs) = delete;
smart_ptr& operator=(const smart_ptr& rhs) = delete;
smart_ptr(smart_ptr&& rhs) : m_ptr(exchange(rhs.m_ptr, nullptr)){}
smart_ptr& operator=(smart_ptr&& rhs){
if (&rhs == this) return *this;
delete m_ptr;
m_ptr = exchange(rhs.m_ptr,nullptr);
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
};
void func()
{
auto ptr = smart_ptr(1, 2); // now ptr guarantee the release of resource
// ...
}
- It's crucial to recognize that, regardless of the function's outcome, the
ptr
object will be automatically destroyed upon the function's termination, regardless of how it concludes. - As a local entity, the
ptr
object will have its destructor invoked during the unwind of the function stack frame, ensuring the proper release of theresource
.
Practical Applications
- RAII facilitates the efficient management of limited resources, such as dynamic memory allocation (
new
/delete
,malloc
/free
), synchronization primitives (acquire/release, mutex lock/unlock), file input/output (open/close), counter increments (++
/--
), database connections (connect/disconnect), or any other resource with limited availability. - Examples from the C++ Standard Library include
std::unique_ptr
,std::ofstream
,std::lock_guard
, and others.
2. Return Type Resolver
Purpose: To deduce the type of the object being initialized or assigned to.
Implementation: Utilizes a templatized conversion operator.
Also referred to as: Return type overloading.
Issue
int from_string(const char *str) { return std::stoi(str); }
float from_string(const char *str) { return std::stof(str); } // error
- A function cannot be overloaded solely based on its return type.
Solution
class from_string
{
const string m_str;
public:
from_string(const char *str) : m_str(str) {}
template
operator type(){
if constexpr(std::is_same_v) return stof(m_str);
else if (std::is_same_v) return stoi(m_str);
}
};
int n_int = from_string("123");
float n_float = from_string("123.111");
// Will only work with C++17 due to `std::is_same_v`
If you’re new to constexpr
, I’ve crafted a concise guide on selecting between const and constexpr in C++.
Practical Applications
- When leveraging
nullptr
(introduced in C++11), this approach operates discreetly to deduce the correct type, contingent on the pointer variable it’s assigned to. - You can also bypass the function overloading constraint based on return type, as exemplified above.
- The Return Type Resolver can also be utilized to provide a generic interface for assignment, independent of the object being assigned.
3. Type Abstraction
Purpose: To create a generic container capable of handling a diverse range of concrete types.
Implementation Strategies: Can be achieved through void*
, templates, polymorphism, union, proxy class, and other means.
Also Referred to as: Duck-typing.
Challenge
- C++ is a statically typed language with robust type checking. In statically typed languages, object types are determined and set at compile-time. In contrast, dynamically typed languages associate types with runtime values.
- In strongly typed languages, an object’s type remains fixed after compilation.
- To overcome this limitation and provide a feature akin to dynamically typed languages, library designers develop various generic container-like solutions, such as
std::any
(C++17),std::variant
(C++17), andstd::function
(C++11), among others.
Variations of Type Abstraction Techniques
- There is no single, rigid rule for implementing this idiom; it can take various forms, each with its own drawbacks, as follows:
=> Type abstraction using void* (similar to C)
void qsort (void* base, size_t num, size_t size,
int (*compare)(const void*,const void*));
Constraint: lacks built-in safety features and necessitates a distinct comparison function for each data type.
=> Attaining Type Erasure through Template Metaprogramming
template
void sort(RandomAccessIterator first, RandomAccessIterator last);
Constraint: may lead to an explosion of function template instantiations, resulting in prolonged compilation times.
=> Type Erasure via Runtime Polymorphism
struct base { virtual void method() = 0; };
struct derived_1 : base { void method() { cout << "derived_1\n"; } };
struct derived_2 : base { void method() { cout << "derived_2\n"; } };
// We don't see a concrete type (it's erased) though can dynamic_cast
void call(base* ptr) { ptr->method(); };
Constraint: incurs runtime performance penalties (dynamic dispatch, indirection, vtable, etc.).
=> Type Erasure using Tagged Unions
struct Data {};
union U {
Data d; // occupies 1 byte
std::int32_t n; // occupies 4 bytes
char c; // occupies 1 byte
~U() {} // need to know currently active type
}; // an instance of U in total occupies 4 bytes.
Constraint: compromises type safety guarantees.
Solution
- As I mentioned earlier, the standard library already provides generic containers with this functionality.
- To gain a deeper understanding of type erasure, let’s implement a container similar to
std::any
, exploring its inner workings:
struct any
{
struct base {};
template
struct inner: base{
inner(T t): m_t{std::forward(t)} {}
T m_t;
static void type() {}
};
any(): m_ptr{nullptr}, typePtr{nullptr} {}
template
any(T && t): m_ptr{std::make_unique>(std::forward(t))}, typePtr{&inner::type} {}
template
any& operator=(T&& t){
m_ptr = std::make_unique>(std::forward(t));
typePtr = &inner::type;
return *this;
}
private:
template
friend T& any_cast(const any& var);
std::unique_ptr m_ptr = nullptr;
void (*typePtr)() = nullptr;
};
template
T& any_cast(const any& var)
{
if(var.typePtr == any::inner::type)
return static_cast*>(var.m_ptr.get())->m_t;
throw std::logic_error{"Invalid cast!"};
}
int main()
{
any var(10);
std::cout << any_cast(var) << std::endl;
var = std::string{"some text"};
std::cout << any_cast(var) << std::endl;
return 0;
}
Notably, we’re employing an empty static method, namely inner::type()
, to ascertain the template instance type in any_cast
, thereby facilitating type identification.
Practical Applications
- Leverage to accommodate multiple types of return values from functions or methods (although this approach is not recommended).
4. Curiously Recurring Template Pattern (CRTP)
Objective: To attain static polymorphism.
Implementation Strategy: Harness base class template specialization. Also referred to as: Upside-down inheritance, Compile-time polymorphism
The Challenge Ahead
struct obj_type_1
{
bool operator<(const value &rhs) const {return m_x < rhs.m_x;}
// bool operator==(const value &rhs) const;
// bool operator!=(const value &rhs) const;
// List goes on. . . . . . . . . . . . . . . . . . . .
private:
// data members for comparison
};
struct obj_type_2
{
bool operator<(const value &rhs) const {return m_x < rhs.m_x;}
// bool operator==(const value &rhs) const;
// bool operator!=(const value &rhs) const;
// List goes on. . . . . . . . . . . . . . . . . . . .
private:
// data members for comparison
};
struct obj_type_3 { ...
struct obj_type_4 { ...
// List goes on. . . . . . . . . . . . . . . . . . . .
- By defining a single comparison operator for each comparable object, we can eliminate redundancy and leverage the
operator<
to overload other operators. - As a result,
operator<
becomes the sole operator that possesses type information, allowing other operators to be type-agnostic and reusable.
Optimized Solution
- Create Reusable Templates by Pattern implementation adheres to a simple principle: separate type-dependent and type-independent functionality, then bind the latter to a base class using template specialization.
- To illustrate this concept more clearly, consider the following solution to the aforementioned problem:
template
struct compare {};
struct value : public compare
{
value(const int x): m_x(x) {}
bool operator<(const value &rhs) const { return m_x < rhs.m_x; }
private:
int m_x;
};
template
bool operator>(const compare &lhs, const compare &rhs) {
// static_assert(std::is_base_of_v, derived>); // Compile time safety measures
return (static_cast(rhs) < static_cast(lhs));
}
/* Same goes with other operators
== :: returns !(lhs < rhs) and !(rhs < lhs)
!= :: returns !(lhs == rhs)
>= :: returns (rhs < lhs) or (rhs == lhs)
<= :: returns (lhs < rhs) or (rhs == lhs)
*/
int main()
{
value v1{5}, v2{10};
cout < v2) << '\n';
return 0;
}
// Now no need to write comparator operators for all the classes,
// Write only type dependent `operator <` & use CRTP
Usecases
- CRTP is widely employed for static polymorphism without bearing the cost of virtual dispatch mechanism. Consider the following code we have not used virtual keyword and still achieved the functionality of polymorphism(specifically static polymorphism).
template
struct animal {
void who() { implementation().who(); }
private:
specific_animal& implementation() {return *static_cast(this);}
};
struct dog : public animal {
void who() { cout << "dog" << endl; }
};
struct cat : public animal {
void who() { cout << "cat" << endl; }
};
template
void who_am_i(animal & animal) {
animal.who();
}
- The Curiously Recurring Template Pattern (CRTP) also yields performance enhancements, as previously discussed, and promotes code reuse.
Update: The aforementioned issue of declaring multiple comparison operators will be permanently resolved in C++20 with the introduction of the spaceship (<=>
)/Three-way-comparison operator.
5. Virtual Constructor
Objective: To create a replica or new object without prior knowledge of its concrete type.
Implementation: Employs overloaded methods with polymorphic assignment.
Also referred to as: Factory method/design pattern.
Problem
- C++ supports polymorphic object destruction via its base class’s virtual destructor. However, equivalent support for object creation and copying is lacking, as C++ does not support virtual constructors or copy constructors.
- Moreover, you cannot create an object unless you know its static type, as the compiler must be aware of the amount of space it needs to allocate. Similarly, copying an object also requires knowledge of its type at compile-time.
struct animal {
virtual ~animal(){ cout<<"~animal\n"; }
};
struct dog : animal {
~dog(){ cout<<"~dog\n"; }
};
struct cat : animal {
~cat(){ cout<<"~cat\n"; }
};
void who_am_i(animal *who) { // not sure whether dog would be passed here or cat
// How to `create` an object of the same type as pointed by who ?
// How to `copy` an object of the same type as pointed by who ?
delete who; // you can delete the object pointed by who
}
Polymorphic Object Creation and Duplication Solution
- The Virtual Constructor technique empowers C++ developers to create and duplicate objects in a polymorphic manner, delegating the responsibility of object creation and duplication to the derived class via virtual methods, thereby ensuring flexibility and extensibility.
- The following code snippet not only implements a virtual constructor (i.e.,
instantiate()
) but also a virtual copy constructor (i.e.,replicate()
), demonstrating a robust approach to object creation and duplication.
struct animal {
virtual ~animal() = default;
virtual std::unique_ptr instantiate() = 0;
virtual std::unique_ptr replicate() = 0;
};
struct dog : animal {
std::unique_ptr instantiate() { return std::make_unique(); }
std::unique_ptr replicate() { return std::make_unique(*this); }
};
struct cat : animal {
std::unique_ptr instantiate() { return std::make_unique(); }
std::unique_ptr replicate() { return std::make_unique(*this); }
};
void who_am_i(animal *who) {
auto new_who = who->instantiate();// `create` an object of the same type as pointed by who ?
auto duplicate_who = who->replicate(); // `copy` an object of the same type as pointed by who ?
delete who; // you can delete the object pointed by who
}
Practical Uses
- To develop a universal interface for instantiating and copying diverse classes using a single class.
6. SFINAE and std::enable_if
Objective: To eliminate functions that do not produce valid template instantiations from a set of overloaded functions.
Implementation: Achieved automatically by the compiler or leveraged using std::enable_if
.
Rationale
- Substitution Failure Is Not An Error is a language feature (not an idiom) employed by a C++ compiler to filter out certain 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 is discarded from the overload set instead of causing a compile error.
- Substitution failure occurs when a type or expression is ill-formed.
template
void func(T* t){ // Single overload set
if constexpr(std::is_class_v){ cout << "T is user-defined type\n"; }
else { cout << "T is primitive type\n"; }
}
int primitive_t = 6;
struct {char var = '4';} class_t;
func(&class_t);
func(&primitive_t);
- How can we create two distinct sets (based on primitive type & user-defined type separately) of a function having the same signature?
Resolution
template>>
void func(T* t){
cout << "T is user-defined type\n";
}
template, T> = 0>
void func(T* t){ // NOTE: function signature is NOT-MODIFIED
cout << "T is primitive type\n";
}
- The above code snippet showcases the strategic application of SFINAE, leveraging
std::enable_if
, where the initial template instantiation is tantamount tovoid func<(anonymous), void>((anonymous) * t)
and the second,void func(int * t)
. - You can explore
std::enable_if
further by visiting this resource.
Practical Uses
- In tandem with
std::enable_if
, SFINAE assumes a vital role in template metaprogramming. - The standard library has also extensively harnessed SFINAE in most type_traits utilities. Consider the following illustration:
// Adapted & condensed from https://stackoverflow.com/questions/982808/c-sfinae-examples.
template
class is_class_type {
template static char test(int C::*);
template static double test(...);
public:
enum { value = sizeof(is_class_type::test(0)) == sizeof(char) };
};
struct class_t{};
int main()
{
cout<::value<::value<
- In the absence of SFINAE, the compiler would throw an error, similar to “
0
cannot be converted to a member pointer for a non-class typeint
“, since the two overloads oftest
differ solely in their return types. - Given that
int
is not a class, it cannot possess a member pointer of typeint int::*
, which is a fundamental constraint.
7. Proxy
Objective: To achieve seamless functionality through the strategic use of an intermediate class.
Implementation: By leveraging a temporary or proxy class, which acts as a bridge.
Also referred to as: operator []
(i.e., subscript) proxy, double or twice operator overloading, which enables a more intuitive interface.
Motivation
- Many mistakenly believe this concept revolves solely around the subscript operator (i.e.,
operator[ ]
), but I firmly believe that the intermediate data exchange type is, in fact, the proxy, which plays a crucial role. - We have already encountered a prime example of this idiom indirectly above in type erasure (i.e., class
any::inner<>
). However, I think an additional example will further solidify our understanding of this concept.
operator [ ] solution
template
struct arr2D{
private:
struct proxy_class{
proxy_class(T *arr) : m_arr_ptr(arr) {}
T &operator { return m_arr_ptr[idx]; }
private:
T *m_arr_ptr;
};
T m_arr[10][10];
public:
arr2D::proxy_class operator { return arr2D::proxy_class(m_arr[idx]); }
};
int main()
{
arr2D<> arr;
arr[0][0] = 1;
cout << arr[0][0];
return 0;
}
Practical Applications
- To develop intuitive features such as operator overloading,
std::any
, and more, leveraging the power of C++.
Summary and FAQs
When to Utilize Resource Acquisition Is Initialization (RAII)?
In scenarios where a series of steps are necessary to accomplish a task, with setup and cleanup being the ideal bookends, RAII proves to be the perfect solution.
Why Is Return Type-Based Function Overloading Not Possible?
Overloading functions based on return type is not feasible since the return value of a function is not always utilized in a function call expression. Consider the following example: get_val();
What action does the compiler take in this case?
When to Apply the Return Type Resolver Idiom?
The return type resolver idiom is particularly useful when input types are fixed, but output types may vary, providing a flexible solution for handling diverse output scenarios.
What Is Type Erasure in C++ Programming?
Type erasure is a technique employed to design generic types that rely on the type of assignment, similar to Python's approach. By the way, are you familiar with auto
, or can you design one now?
Best Scenarios for Applying Type Erasure Idiom?
It's particularly useful in generic programming and can also be employed to handle multiple types of return values from functions or methods, although this approach is not recommended due to its limitations.
What Is the Curiously Recurring Template Pattern (CRTP)?
The CRTP occurs when a class A
has a base class, and that base class is a template specialization for the class A
itself, creating a recursive relationship. For example, template class X{...}; class A : public X {...};
Isn't it curiously recurring?
Why Does the Curiously Recurring Template Pattern (CRTP) Work?
I think this answer provides a suitable explanation.
What Is SFINAE (Substitution Failure Is Not An Error)?
Substitution Failure Is Not An Error is a language feature that C++ compilers utilize to filter out certain templated function overloads during overload resolution, ensuring efficient and accurate function calls.
What Is a Proxy Class in C++ Programming?
A proxy serves as an intermediary class, offering a tailored interface to another class, providing a layer of abstraction and flexibility.
Why Is a Virtual Constructor Absent in C++?
A virtual table (vtable) is generated for each class that incorporates one or more virtual functions. When an object of such a class is instantiated, it contains a virtual pointer, which references the base of the corresponding vtable. This vtable is utilized to resolve the function address during virtual function calls, ensuring efficient and accurate polymorphism.
A constructor's virtuality is impossible due to the fact that, at the point of its execution, the vtable remains absent from memory. As a result, no virtual pointer has been established yet, making it essential for constructors to be non-virtual.
Is it possible to declare a class's copy constructor as virtual in C++?
This inquiry bears a striking resemblance to the previous question, "Why does C++ lack virtual constructors?", which has already been addressed above.
What are the practical applications and necessity of virtual constructors?
The fundamental objective of virtual constructors is to facilitate the creation and replication of objects (in the absence of prior knowledge regarding their concrete type) via a polymorphic method, leveraging a base class.
Do you have any suggestions, queries, or simply wish to say Hi
?
Top comments (0)