Discussion: A Strong Point
Execute the code to understand the output and gain insights into strong typing and implicit conversions.
Run the code
Now, it’s time to execute the code and observe the output.
#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.