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.
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)) {}