Discussion: A Strong Point

Run the code

Now, it’s time to execute the code and observe the output.

Press + to interact
#include <iostream>
struct Points
{
Points(int value) : value_(value) {}
int value_;
};
struct Player
{
explicit Player(Points points) : points_(points) {}
Points points_;
};
int main()
{
Player player(3);
std::cout << player.points_.value_;
}

Strong typing

The Player struct has a points_ member to keep track of the player’s points. Instead of using a fundamental type like int for this, we use a custom type Points. This technique is often called strong typing and can make the code easier to read and avoid bugs.

Benefits of strong typing

For instance, given a do_something(int player_id, int points, int lives) function, it can be easy to slip up and pass it player_id, lives, points instead, causing the number of lives to be used for points and vice versa. When we use separate strong types for these values, a mistake like this would cause the program to fail to compile rather than create a bug.

Understanding the output

The question in this puzzle is whether we’re allowed to construct a Player object directly from an int, like we do in the Player player(3) expression, or whether we need to manually create an object of the strong type Points to pass to the Player constructor.

Solution: Implicit conversion and direct initialization

For Player player(3) to work, we need an implicit conversion from int. Only constructors not marked explicit, known as converting constructors, can participate in implicit conversions. The constructor of Player is marked explicit and is thus not a converting constructor, so how does this conversion work?

The Player player(3) form is an example of direct-initialization (so is Player player{3} but not, for example, Player player = 3). We like to think that we’re directly calling the constructor of Player. In direct-initialization, overload resolution is used to pick the best constructor. When we’re direct-initializing a Player from an int, overload resolution tries to find an implicit conversion sequence from int to each of the constructors of Player. The Player has only one constructor, which takes a Points, so we need an implicit conversion from int to Points. The constructor of Points isn’t explicit, so it’s a converting constructor and can be used in the implicit conversion sequence. 3 is implicitly converted to Points, and this Points object is passed to the constructor of Player.

The explicit keyword

The constructor of Player itself is not involved in the implicit conversion; we’re calling it directly. So, it makes no difference whether or not it’s explicit.

On the other hand, if it was the constructor of Points that was explicit, it would no longer be a converting constructor and would not be usable in the implicit conversion sequence. We’d no longer be able to implicitly convert the int 3 to Points, and the program would not compile.

Giving our strong types explicit constructors makes the code slightly more verbose but also more robust. Returning to the do_something example, we would now have to write do_something(PlayerId{42}, Points{3}, Lives{1}) for the program to compile, whereas with converting constructors, we could still slip up and write do_something(42, 1, 3) and the program would still compile. We also find the former more readable.

Copy-initialization vs. direct-initialization

What would happen if we instead wrote this:

Player player = 3

This form is called copy-initialization and differs a bit from direct-initialization. Now, we’re no longer calling the Player construct directly; we’re instead trying to implicitly convert an int to Player. An implicit conversion sequence can only involve one user-defined conversion, but we would need one from int to Player and then a second one from Player to Points. So Player player = 3 would never compile, regardless of whether any of the constructors are explicit.

Recommendations

Here are some recommendations to ensure robust and maintainable code.

  • Mark single-argument constructors as explicit: This prevents unintended implicit conversions, reducing the risk of subtle bugs.

  • Use strong types: Prefer strong types over fundamental types to make your code safer and more self-documenting, helping to avoid bugs related to type misuse.

  • Leverage a strong type library: Consider using a strong type library, such as Björn Fahller’s strong_type, to reduce boilerplate code when creating strong types.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.