A constructor taking a single argument can normally be used as an implicit conversion to the class type. The compiler will automatically invoke it whenever it needs to convert a value of the argument type to the class type. Sometimes the conversion is explicitly desired; other times, it is a silent conversion with the potential to change the program's meaning.
Let's have the following class that wraps an integer and has a single-argument constructor for an int.
#include <iostream>
// Define the members that belong to this type
class Number
{
private:
int value{};
public:
Number(int n)
{
value = n;
std::cout << "Number created from " << value << '\n';
}
void print()
{
std::cout << value << '\n';
}
};
void takeNumber(Number n)
{
n.print();
}
int main()
{
takeNumber(5);
return 0;
}
Calling the function takeNumber normally requires a Number argument. In main, the call is written using the integer 5. The compiler knows it can convert an int to a Number using Number(int) and does so to satisfy the function's signature.
The constructor then executes, printing "Number created from 5", followed by the function printing the value again.
As you can see, the conversion was unrequested. The language simply made it because the constructor was available.
This automatic conversion can happen anytime a Number is expected: assignment, return statements, or recursive function calls. When the conversion is not desired, it can make debugging difficult. A function intended to take only fully formed Number objects suddenly accepts raw integers, and the origin of the Number is hidden.
The keyword explicit turns off this implicit conversion. After the keyword has been used on a constructor, the compiler will no longer invoke the constructor implicitly.
#include <iostream>
class Number
{
private:
int value{};
public:
explicit Number(int n)
{
value = n;
std::cout << "Number created from " << value << '\n';
}
void print()
{
std::cout << value << '\n';
}
};
void takeNumber(Number n)
{
n.print();
}
int main()
{
// takeNumber(5); // Compile error
takeNumber(Number{ 5 }); // Allowed
Number n{ 10 }; // Also allowed
n.print();
return 0;
}
The constructor is now declared explicitly: explicit Number(int n). The previously acceptable call takeNumber(5) fails at compile time because it relies on the constructor being automatically invoked. The call should be rewritten using the explicit temporary takeNumber(Number{ 5 }) or the object should be constructed using direct initialization syntax: Number n{ 10 };. The constructor still runs and prints its message; the only difference is the programmer is made aware when an object of type Number is created from an int.
Any constructor taking two or more arguments is never a converting constructor; therefore, the keyword explicit has no effect on them. Any single-argument constructor, or any multi-argument constructor for which every argument has a default value, is a converting constructor unless it is declared explicit. Whether a constructor is a converting constructor is thus dependent only on two conditions: (1) whether or not the constructor was marked as explicit, and (2) the number of arguments that the constructor actually took, after all defaults were filled.
The two different initializations illustrated here fall into the same rule. Direct initialization, with curly braces and parenthesis, is allowed to use an explicit constructor. Copy initialization, using an equals sign, is not.
#include <iostream>
class Number
{
private:
int value{};
public:
explicit Number(int n)
{
value = n;
}
void print()
{
std::cout << value << '\n';
}
};
int main()
{
Number a{ 7 }; // Direct initialization, okay
// Number b = 7; // Copy initialization, compile error
a.print();
return 0;
}
The initialization with braces Number a{ 7 }; is direct initialization and correctly succeeded. Initialization with the equals sign is copy initialization, and because the constructor was explicit, the compiler generated an error. The rule maintains explicitness: explicit constructors can only be called when the syntax already indicates an explicit intent to call the constructor.
If you have a constructor whose conversion from an argument type to a class type is not considered inherently natural, marking it as explicit is the conventional choice.
0 comments
No comments yet. Be the first.