#118 – Converting constructors and the case for the explicit keyword

Three things to take away:

  • A non-explicit constructor can enable implicit conversion into a class type.
  • Only one user-defined conversion is allowed in an implicit conversion sequence.
  • Mark single-argument constructors explicit unless implicit conversion is genuinely intended.

The problem

Consider the following:

#include <iostream>

class RareMacGuffin {
private:
    int m_rarityLevel {};

public:
    RareMacGuffin(int rarityLevel)
        : m_rarityLevel { rarityLevel }
    {
    }

    int rarityLevel() const{
        return m_rarityLevel;
    }
};

void inspect(RareMacGuffin item){
    std::cout << "Rarity level: " << item.rarityLevel() << '\n';
}

int main(){
    inspect(true);
}

Terminal output:
Rarity level: 1

This compiles. Even though it is semantically meaningless.

C++ uses implicit conversions and converts true to int, then uses the constructor:

RareMacGuffin(int rarityLevel)

to create a temporary RareMacGuffin.

The result is technically correct but obviously it’s not what we want.

What is a converting constructor?

converting constructor is a constructor that allows an object to be created from another type through implicit conversion. This is what occurred in the previous example.

A RareMacGuffin(int rarityLevel) could be created by constructing it with a bool type.

A useful implicit conversion

Not every converting constructor is bad.

For example, a lightweight type that wraps a string-like value may reasonably accept std::string_view:

#include <iostream>
#include <string>
#include <string_view>

class Username {
private:
    std::string m_name {};

public:
    Username(std::string_view name)
        : m_name { name }
    {
    }

    const std::string& value() const{
        return m_name;
    }
};

void greet(Username username){
    std::cout << "Hello, " << username.value() << '\n';
}

int main(){
    greet(Username { "Ada" });
}

Terminal output:
Hello, Ada

This version is explicit at the call site:

greet(Username { "Ada" });

The reader can see that a Username is being constructed.

If the constructor were not explicit, the function may also accept a std::string_view implicitly. Whether that is desirable depends on the API.

The design question is:

Should callers be allowed to pass this other type as if it were already my class type?

If the answer is no, use explicit.

Only one user-defined conversion is allowed

C++ allows at most one user-defined conversion in an implicit conversion sequence.

This prevents long chains of automatic conversions from silently constructing distant types.

Consider this example:

#include <iostream>

class UserId {
private:
    int m_value {};

public:
    UserId(int value)
        : m_value { value }
    {
    }

    int value() const{
        return m_value;
    }
};

class UserProfile {
private:
    UserId m_id;

public:
    UserProfile(UserId id)
        : m_id { id }
    {
    }

    void print() const{
        std::cout << "User ID: " << m_id.value() << '\n';
    }
};

void printProfile(UserProfile profile){
    profile.print();
}

int main(){
    printProfile(42);
}

Compiler error, abridged:
could not convert '42' from 'int' to 'UserProfile'

The call looks simple:

printProfile(42);

But making it work implicitly would require two user-defined conversions:

int    → UserId
UserId → UserProfile

C++ does not allow that chain implicitly.

The fix is to make the construction explicit:

#include <iostream>

class UserId {
private:
    int m_value {};

public:
    UserId(int value)
        : m_value { value }
    {
    }

    int value() const{
        return m_value;
    }
};

class UserProfile {
private:
    UserId m_id;

public:
    UserProfile(UserId id)
        : m_id { id }
    {
    }

    void print() const{
        std::cout << "User ID: " << m_id.value() << '\n';
    }
};

void printProfile(UserProfile profile){
    profile.print();
}

int main(){
    printProfile(UserProfile { UserId { 42 } });
}

Terminal output:
User ID: 42

Now the conversions are not hidden.

The caller says exactly what is being constructed:

UserProfile { UserId { 42 } }

That is clearer and safer.

Blocking implicit construction with explicit

The explicit keyword prevents a constructor from being used for implicit conversions.

Return to the RareMacGuffin example:

#include <iostream>

class RareMacGuffin {
private:
    int m_rarityLevel {};

public:
    explicit RareMacGuffin(int rarityLevel)
        : m_rarityLevel { rarityLevel }
    {
    }

    int rarityLevel() const{
        return m_rarityLevel;
    }
};

void inspect(RareMacGuffin item){
    std::cout << "Rarity level: " << item.rarityLevel() << '\n';
}

int main(){
    inspect(true);
}

Compiler error, abridged:
could not convert 'true' from 'bool' to 'RareMacGuffin'

The constructor still exists:

explicit RareMacGuffin(int rarityLevel)

But it can no longer be used implicitly. There is no accidental conversion from bool.

Takeaway

A converting constructor allows C++ to create a class object from another type.

That can be convenient, but it permits construction of meaningless objects. explicit can be used to enforce object creation.