Before C++11, a constructor with a single parameter was considered a converting constructor. 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 these are useful in some cases, but can create problems in other cases. In this recipe, we will see how to use explicit constructors and conversion operators.
To understand why explicit constructors are necessary and how they work, we will first look at converting constructors. The following class 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 printing a message. As of C++11, these are all considered converting constructors. The class also has a conversion operator that converts the type to a bool
:
struct foo
{
foo()
{ std::cout << "foo" << std::endl; }
foo(int const a)
{ std::cout << "foo(a)" << std::endl; }
foo(int const a, double const b)
{ std::cout << "foo(a, b)" << std::endl; }
operator bool() const { return true; }
};
Based on this, the following definitions of objects are possible (note that the comments represent the console 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)
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.
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)" << std::endl; }
In this case, f5
and f6
will print foo(l)
, while f8
and f9
will generate compiler errors because all elements of the initializer list should be integers.
These may all look right, but the implicit conversion constructors enable scenarios where the implicit conversion may not be what we wanted:
void bar(foo const f)
{
}
bar({}); // foo()
bar(1); // foo(a)
bar({ 1, 2.0 }); // foo(a, b)
The conversion operator to bool
in the example above also enables us to use foo
objects where boolean values are expected:
bool flag = f1;
if(f2) {}
std::cout << f3 + f4 << std::endl;
if(f5 == f6) {}
The first two are examples where foo
is expected to be used as boolean but 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. The class may provide 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, such a string buffer could look 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 sb1; // empty buffer
string_buffer sb2(20); // buffer of 20 characters
string_buffer sb3(str.get());
// buffer initialized from input parameter
sb1
is created using the default constructor and thus has an empty buffer; sb2
is initialized using the constructor with a single parameter and the value of the parameter represents the size in characters of the internal buffer; sb3
is initialized with an existing buffer and that is used to define the size of the internal buffer and to 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.
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 earlier to declare all constructors 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 is minimal, but the definitions of b4
and b5
in the earlier example no longer work, and are incorrect, since the implicit conversion 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.
Note
This is only the case when initialization is done with copy initialization and not when using the 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
, the string_buffer
objects defined earlier, the following are no longer possible with explicit conversion operator bool
:
std::cout << b1 + b2 << std::endl;
if(b1 == b2) {}
Instead, they require explicit conversion to bool
:
std::cout << static_cast<bool>(b1) + static_cast<bool>(b2);
if(static_cast<bool>(b1) == static_cast<bool>(b2)) {}