Book Image

Modern C++ Programming Cookbook - Second Edition

By : Marius Bancila
5 (1)
Book Image

Modern C++ Programming Cookbook - Second Edition

5 (1)
By: Marius Bancila

Overview of this book

C++ has come a long way to be one of the most widely used general-purpose languages that is fast, efficient, and high-performance at its core. The updated second edition of Modern C++ Programming Cookbook addresses the latest features of C++20, such as modules, concepts, coroutines, and the many additions to the standard library, including ranges and text formatting. The book is organized in the form of practical recipes covering a wide range of problems faced by modern developers. The book also delves into the details of all the core concepts in modern C++ programming, such as functions and classes, iterators and algorithms, streams and the file system, threading and concurrency, smart pointers and move semantics, and many others. It goes into the performance aspects of programming in depth, teaching developers how to write fast and lean code with the help of best practices. Furthermore, the book explores useful patterns and delves into the implementation of many idioms, including pimpl, named parameter, and attorney-client, teaching techniques such as avoiding repetition with the factory pattern. There is also a chapter dedicated to unit testing, where you are introduced to three of the most widely used libraries for C++: Boost.Test, Google Test, and Catch2. By the end of the book, you will be able to effectively leverage the features and techniques of C++11/14/17/20 programming to enhance the performance, scalability, and efficiency of your applications.
Table of Contents (16 chapters)
13
Bibliography
14
Other Books You May Enjoy
15
Index

Using explicit constructors and conversion operators to avoid implicit conversion

Before C++11, a constructor with a single parameter was considered a converting constructor (because it takes a value of another type and creates a new instance of the type out of it). With C++11, every constructor without the explicit specifier is considered a converting constructor. Such a constructor defines an implicit conversion from the type or types of its arguments to the type of the class. Classes can also define converting operators that convert the type of the class to another specified type. All of these are useful in some cases but can create problems in other cases. In this recipe, we will learn how to use explicit constructors and conversion operators.

Getting ready

For this recipe, you need to be familiar with converting constructors and converting operators. In this recipe, you will learn how to write explicit constructors and conversion operators to avoid implicit conversions to and from a type. The use of explicit constructors and conversion operators (called user-defined conversion functions) enables the compiler to yield errors—which, in some cases, are coding errors—and allow developers to spot those errors quickly and fix them.

How to do it...

To declare explicit constructors and explicit conversion operators (regardless of whether they are functions or function templates), use the explicit specifier in the declaration.

The following example shows both an explicit constructor and an explicit converting operator:

struct handle_t
{
  explicit handle_t(int const h) : handle(h) {}
  explicit operator bool() const { return handle != 0; };
private:
  int handle;
};

How it works...

To understand why explicit constructors are necessary and how they work, we will first look at converting constructors. The following class, foo, has three constructors: a default constructor (without parameters), a constructor that takes an int, and a constructor that takes two parameters, an int and a double. They don't do anything except print a message. As of C++11, these are all considered converting constructors. The class also has a conversion operator that converts a value of the foo type to a bool:

struct foo
{
  foo()
  { std::cout << "foo" << '\n'; }
  foo(int const a)
  { std::cout << "foo(a)" << '\n'; }
  foo(int const a, double const b)
  { std::cout << "foo(a, b)" << '\n'; }
  operator bool() const { return true; }
};

Based on this, the following definitions of objects are possible (note that the comments represent the console's output):

foo f1;              // foo()
foo f2 {};           // foo()
foo f3(1);           // foo(a)
foo f4 = 1;          // foo(a)
foo f5 { 1 };        // foo(a)
foo f6 = { 1 };      // foo(a)
foo f7(1, 2.0);      // foo(a, b)
foo f8 { 1, 2.0 };   // foo(a, b)
foo f9 = { 1, 2.0 }; // foo(a, b)

The variables f1 and f2 invoke the default constructor. f3, f4, f5, and f6 invoke the constructor that takes an int. Note that all the definitions of these objects are equivalent, even if they look different (f3 is initialized using the functional form, f4 and f6 are copy initialized, and f5 is directly initialized using brace-init-list). Similarly, f7, f8, and f9 invoke the constructor with two parameters.

In this case, f5 and f6 will print foo(l), while f8 and f9 will generate compiler errors because all the elements of the initializer list should be integers.

It may be important to note that if foo defines a constructor that takes an std::initializer_list, then all the initializations using {} would resolve to that constructor:

foo(std::initializer_list<int> l)
{ std::cout << "foo(l)" << '\n'; }

These may all look right, but the implicit conversion constructors enable scenarios where the implicit conversion may not be what we wanted. First, let's look at some correct examples:

void bar(foo const f)
{
}
bar({});             // foo()
bar(1);              // foo(a)
bar({ 1, 2.0 });     // foo(a, b)

The conversion operator to bool from the foo class also enables us to use foo objects where Boolean values are expected. Here is an example:

bool flag = f1;                // OK, expect bool conversion
if(f2) { /* do something */ }  // OK, expect bool conversion
std::cout << f3 + f4 << '\n';  // wrong, expect foo addition
if(f5 == f6) { /* do more */ } // wrong, expect comparing foos

The first two are examples where foo is expected to be used as a Boolean. However, the last two with addition and test for equality are probably incorrect, as we most likely expect to add foo objects and test foo objects for equality, not the Booleans they implicitly convert to.

Perhaps a more realistic example to understand where problems could arise would be to consider a string buffer implementation. This would be a class that contains an internal buffer of characters.

This class provides several conversion constructors: a default constructor, a constructor that takes a size_t parameter representing the size of the buffer to preallocate, and a constructor that takes a pointer to char that should be used to allocate and initialize the internal buffer. Succinctly, the implementation of the string buffer that we use for this exemplification looks like this:

class string_buffer
{
public:
  string_buffer() {}
  string_buffer(size_t const size) {}
  string_buffer(char const * const ptr) {}
  size_t size() const { return ...; }
  operator bool() const { return ...; }
  operator char * const () const { return ...; }
};

Based on this definition, we could construct the following objects:

std::shared_ptr<char> str;
string_buffer b1;            // calls string_buffer()
string_buffer b2(20);        // calls string_buffer(size_t const)
string_buffer b3(str.get()); // calls string_buffer(char const*)

The object b1 is created using the default constructor and thus has an empty buffer; b2 is initialized using the constructor with a single parameter where the value of the parameter represents the size in terms of the characters of the internal buffer; and b3 is initialized with an existing buffer, which is used to define the size of the internal buffer and copy its value into the internal buffer. However, the same definition also enables the following object definitions:

enum ItemSizes {DefaultHeight, Large, MaxSize};
string_buffer b4 = 'a';
string_buffer b5 = MaxSize;

In this case, b4 is initialized with a char. Since an implicit conversion to size_t exists, the constructor with a single parameter will be called. The intention here is not necessarily clear; perhaps it should have been "a" instead of 'a', in which case the third constructor would have been called.

However, b5 is most likely an error, because MaxSize is an enumerator representing an ItemSizes and should have nothing to do with a string buffer size. These erroneous situations are not flagged by the compiler in any way. The implicit conversion of unscoped enums to int is a good argument for preferring to use scoped enums (declared with enum class), which do not have this implicit conversion. If ItemSizes was a scoped enum, the situation described here would not appear.

When using the explicit specifier in the declaration of a constructor, that constructor becomes an explicit constructor and no longer allows implicit constructions of objects of a class type. To exemplify this, we will slightly change the string_buffer class to declare all constructors as explicit:

class string_buffer
{
public:
  explicit string_buffer() {}
  explicit string_buffer(size_t const size) {}
  explicit string_buffer(char const * const ptr) {}
  explicit operator bool() const { return ...; }
  explicit operator char * const () const { return ...; }
};

The change here is minimal, but the definitions of b4 and b5 in the earlier example no longer work and are incorrect. This is because the implicit conversions from char or int to size_t are no longer available during overload resolution to figure out what constructor should be called. The result is compiler errors for both b4 and b5. Note that b1, b2, and b3 are still valid definitions, even if the constructors are explicit.

The only way to fix the problem, in this case, is to provide an explicit cast from char or int to string_buffer:

string_buffer b4 = string_buffer('a');
string_buffer b5 = static_cast<string_buffer>(MaxSize);
string_buffer b6 = string_buffer{ "a" };

With explicit constructors, the compiler is able to immediately flag erroneous situations and developers can react accordingly, either fixing the initialization with a correct value or providing an explicit cast.

This is only the case when initialization is done with copy initialization and not when using functional or universal initialization.

The following definitions are still possible (and wrong) with explicit constructors:

string_buffer b7{ 'a' };
string_buffer b8('a');

Similar to constructors, conversion operators can be declared explicit (as shown earlier). In this case, the implicit conversions from the object type to the type specified by the conversion operator are no longer possible and require an explicit cast. Considering b1 and b2, which are the string_buffer objects we defined earlier, the following are no longer possible with an explicit conversion operator bool:

std::cout << b4 + b5 << '\n'; // error
if(b4 == b5) {}               // error

Instead, they require explicit conversion to bool:

std::cout << static_cast<bool>(b4) + static_cast<bool>(b5);
if(static_cast<bool>(b4) == static_cast<bool>(b5)) {}

The addition of two bool values does not make much sense. The preceding example is intended only to show how an explicit cast is required in order to make the statement compile. The error issued by the compiler when there is no explicit static cast, should help you figure out that the expression itself is wrong and something else was probably intended.

See also

  • Understanding uniform initialization to see how brace-initialization works