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 class out of it). With C++11, every constructor without the explicit
specifier is considered a converting constructor. This is important because 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 (although compilers may have options to ignore some warnings, such as -Wno-narrowing
for GCC) 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 a 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, one with addition and one with a 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 pre-allocate, and a constructor that takes a pointer to char
, which 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) { data.resize(size); }
string_buffer(char const * const ptr) : data(ptr) {}
size_t size() const { return data.size(); }
operator bool() const { return !data.empty(); }
operator char const * () const { return data.c_str(); }
private:
std::string data;
};
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) { data.resize(size); }
explicit string_buffer(char const * const ptr) :data(ptr) {}
size_t size() const { return data.size(); }
explicit operator bool() const { return !data.empty(); }
explicit operator char const * () const { return data.c_str(); }
private:
std::string data;
};
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 (but 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 is no longer possible with an explicit operator bool
conversion:
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