Introduction

In some languages it is common to have conversions between different types such as converting an integer to a floating point, for example. There are two types of conversions in programming; implicit conversions and explicit conversions.

Explicit conversions

An explicit conversions, as the name suggest, it is when the programmer have to explicitly specify what conversion has to be done. This way the code may look more messy but the programmer has the control of the code in all time.

C++
void floatToInt(int x) {
    std::cout << x; // prints 42
}

int main() {
    float x = 42.51231;
    floatToInt(static_cast<int>(x));
}

Implicit conversions

Implicit conversions are a powerfull tool that some languages provide usually to make the code easier to read and more agile when coding. Implicit conversions as the name suggest are done behind the scenes, wich means that the language itself knows that one type has to be converted to another type. In the code snippet below we can see an example:

C++
void floatToInt(int x) {
    std::cout << x; // prints 42
}

int main() {
    float x = 42.51231;
    floatToInt(x);
}

In this case, we can see how easily is to lose information in some data types if you rely in implicit conversion too much. This is te reason some modern languages like Rust doesn’t allow implicit conversions.

“explicit” specifier

explicit specifier is a keyword that C++ provides and can be used in constructors and conversion functions to avoid undisered implicit conversions.

C++
struct X {
    X(int) {}
    operator std::string() const { return std::string();}
};

struct Y {
    explicit Y(int) {}
    explicit operator std::string() const {return std::string();}
};
 
int main() {
    X x1 = 10;
    X x2(10);
    X x3 = {10};
    std::string s1 = x1;
    std::string s2 = static_cast<std::string>(x1);

    Y y1 = 10; // Error: cannot convert int to Y
    Y y2(10);
    Y y3 = {10}; // Error: cannot convert initializer-list to Y
    std::string s3 = y2; // Error: Implicit conversion not implemented.
    std::string s4 = static_cast<std::string>(y2);
}

In an effort for C++ to prevent issues with implicit conversion we have the ‘uniform initialization’ or ‘braced initialization’ in C++ 11 with the operator{}. This operator forze us to expecify the exact type our constructor is expecting.

C++
struct Z {
    Z(int, bool) {}
};

int main() {
    int x1(10.5); // Implicit conversion from double to int -> 10
    int x2{10.5}; // Error: narrowing conversion from double to int.
    
    Z z1(10.5, -1);
    Z z2{10, -1}; // Error: narrowing conversion int to bool.
    Z z3{10, false};
}

Explicit functions

But since ‘braced initialization’ only applies when constructing a type or an object, if we want a specific function to only accept the type we are indicating, the solution is a little bit more tricky.

1. Deleting function overloads
C++
struct Z {
    Z() = default;
    void Foo(int) {}
    void Foo(float) = delete;
    void Foo(bool) = delete;
};

int main() {
    Z z1;
    z1.Foo(1);
    z1.Foo(1.5); // Error: use of deleted function
    z1.Foo(true); // Error: use of deleted function
}

We can use generic parametrization to ensure that only functions we declare are going to be called.

C++
struct Z {
    Z() = default;
    void Foo(int) {}
    
    template<typename T>
    void Foo(T) = delete;
};

int main() {
    Z z1;
    z1.Foo(1);
    z1.Foo(true); // Error: use of deleted function
    z1.Foo(1.5); // Error: use of deleted function
    z1.Foo('a'); // Error: use of deleted function
}
2. Concepts

In C++ 20 concepts were added and it is the proper way to address this problem. Let’s see an example:

C++
template<typename T> 
    requires std::same_as<T, int>
void Foo(T) {}

But we can do it in a shorter way using auto.

C++
concept is_integer = std::same_as<T, int>;

void Foo(is_integer auto) {}

User defined conversions in C++

C++ is one of those languages that give us tools to play with almos every aspect of an implementation. Usually, programming languages with type conversions have some conversiones already defined by the standard. In this case, C++ allow us to define our owns conversions. In the next code snippet we can see an explicit and implicit conversion between two custom types. Both structs recieve 2 std::strings& and 1 int and implement custom casting to std::string.

C++
struct FullNameExplicit {
    explicit FullNameExplicit(const std::string& name, const std::string& second_name, int age) :
    name(name),
    second_name(second_name),
    age(age) {}

    explicit operator std::string() const {
        return name + ' ' + second_name + " has " + std::to_string(age) + '\n';
    }

    std::string name;
    std::string second_name;
    int age;
};

struct FullNameImplicit {
    FullNameImplicit(const std::string& name, const std::string& second_name, int age) :
    name(name),
    second_name(second_name),
    age(age) {}

    operator std::string() const {
        return name + ' ' + second_name + " has " + std::to_string(age) + '\n';
    }

    std::string name;
    std::string second_name;
    int age;
};

void Foo(const std::string person) {
    std::cout << person;
}

int main() {
    FullNameExplicit fne("Ruben", "Rubio", 24);
    Foo(fne); // Error: implicit conversion not defined.
    Foo(static_cast<std::string>(fne));
    
    FullNameImplicit fni("Ruben", "Rubio", 24);
    Foo(fni);
    Foo(static_cast<std::string>(fni));
}

Conclusion

In my humild opinion, I think we should avoid implicit conversion as much as possible. Particularly, when working with other people involved in the code they may not be aware of the implicit conversions you are using and this can tend to get lost easily in a large project.

References

  1. https://en.cppreference.com/w/cpp/language/cast_operator
  2. https://en.cppreference.com/w/cpp/language/explicit
  3. https://stackoverflow.com/questions/121162/what-does-the-explicit-keyword-mean

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *