Guidelines for constructor and cast design

A while back — but sadly not too many blog posts ago — I wrote about explicit constructors and how to handle assignment. In this blog post, I made the assumption that you most likely want to have explicit single argument constructors.

But when do we actually want implicit single argument constructors?

Let’s consider the broader question: How should I design a cast operation for my user-defined type? And how should I design a constructor?

But first, something different: what is the difference between a cast and a constructor?

Casts vs constructors

It might seem silly to ask for the difference between a cast and a constructor.

I mean, this is a cast:

auto i = static_cast<int>(4.0);

And this invokes a constructor:

auto my_vector = std::vector<int, my_allocator<int>>(my_alloc);

However, the same cast can look like a constructor invocation:

auto i = int(4.0);

And the constructor can look like a cast:

auto my_vector = static_cast<std::vector<int, my_allocator<int>>>(my_alloc);

So what is the difference?

It is a semantic difference, not a syntactic difference.

A constructor is any operation that takes any number of arguments and creates a new object of a given type using those arguments. The value of the new object is created using the values of the arguments, but there is no direct connection between the argument values and the new value. Constructors in C++ are usually implemented using, well, constructors — the C++ language feature. But they don’t have to, as we’ll see.

A cast operation also follows that definition of a constructor. But it is special in two ways: First, it only and always takes a single argument of a different type than the one returned. Second, it fundamentally doesn’t change the value of the argument, just the type.

Let me elaborate on the last one a bit. For the sake of this discussion, a value is the abstract concept like the number four. The static_cast<int>(4.0) takes that value stored as a double and returns an int object still containing the same value — the number four. The value didn’t change, only the representation of that value changed.

Of course, this is not always possible. If we write static_cast<int>(4.1), the value “number 4.1” cannot be stored in an int. This is an example of a narrowing cast. How the cast operation behaves in this situation — throw an exception, round to the “nearest value” whatever that is — is up to the implementation. In contrast, a wide cast would be something like static_cast<long>(4): All possible values of an int can be represented as a long, so it will always succeed.

Casts in C++ are usually implemented with a conversion operator or a free function. But note that they can also be implemented using a C++ constructor — this lead to the confusion earlier.

Using those definitions, the following operations are all casts. While they do create a new object the stored value itself is fundamentally the same.

// the double to int example from above
auto i = static_cast<int>(4.0);

// convert the value "Hello World!" from a character array to a `std::string`
std::string str = "Hello World!";

// convert some pointer value to a unique pointer of the same value
// value didn't change, only ownership is new
std::unique_ptr<int> unique_ptr(some_ptr);

// convert the integer value from above to an optional
// again: no change in value, just represented in a new type that can fit an additional value
std::optional<int> my_opt(i);

Note that a cast operation doesn’t need to contain the word “cast” anywhere!

But here we are using a constructor:

// the vector value from above
auto my_vector = std::vector<int, my_allocator<int>>(my_alloc);

// create a string using an integer and a character
std::string my_string(10, 'a');

// create a string stream using the string from above
std::stringstream stream(my_string);

So with the technicality out of the way, let’s take a closer look at the way casts are handled in C++.

Implicit conversions

A single argument constructor that isn’t marked explicit or a non-explicit conversion operator can be used in an implicit conversion. Basically, the compiler will adjust the types without you needing to do anything. Sometimes you don’t even realize it!

Implicit conversions don’t require any extra typing, so they will happen accidentally at some point. So only add new implicit conversions when they have the following properties:

A good example of an implicit conversion is Tstd::optional<T>. It is relatively cheap, there are no preconditions and it should be possible to change a function taking a T at some point to a function taking an optional T.

A negative example would be unsignedint — it leads to a lot of problems! — or even const char*std::string — it requires a non-null pointer and is expensive due to a dynamic memory allocation. But the first was inherited from C and the second is just too convenient.

Directly following from that guideline is this one:

Make single-argument constructors explicit by default!

clang-tidy rule google-explicit-constructor really helps.

Although they should have used an attribute [[implicit]] instead of requiring a comment /* implicit */.

C++ casts

In C there was only a single syntax to convert an object of one type into another type: (new_type)old_object. C++ as a bigger and better language added four new ones:

It also has a new syntax for C style casts — T(old_object) which looks like a constructor call, but may do all C style conversions — but let’s ignore C style casts, they do nothing that can’t be done with the C++ casts.

Of course, C++ being C++: This isn’t true, they can convert an object to a private base class. But you shouldn’t know that.

Of the four new C++ casts operation, I only like one. Can you guess which one?

Wrong, it’s reinterpret_cast.

“But why?”, you ask, “reinterpret_cast is an evil tool, you shouldn’t use that.”

This might be true, but reinterpret_cast only does one thing: It changes a pointer type. The other casts do multiple things at once.

To be fair, reinterpret_cast can also convert a pointer to an integer. But I wanted to say something positive about one cast.

Consider const_cast: It has two similar yet very different jobs — it can be used to add constness and to remove constness. The first is a completely harmless situation and used to help overload resolution sometimes. The second is a dangerous road to undefined behavior if you don’t know what you’re doing. Yet the two modes share the same function name!

C++17 adds std::add_const() as a harmless way of adding constness, which is good, but 20 years too late.

To be fair, I can count my number of const_cast uses with one hand, so it doesn’t really matter.

dynamic_cast is similar: Depending on the types it is used with, it can cast up the hierarchy, down the hierarchy, across entire classes or give you a void* to the most derived object. Those are separate functionality, so why move it all in to one? They should have been a up_cast, down_cast, cross_cast and get_most_derived_ptr functions instead.

But the worst of them is static_cast. It can be used to:

This is probably not an exhaustive list.

These are a lot of different conversions, some are narrowing (floatint), some are wide (T*void*). Some are cheap (uint32_tuint64_t), some are expensive (std::string_viewstd::string). Just looking at the cast in the source code the semantics are impossible to know.

And more importantly, it is not really “static”.

In a way, this is only slightly better than an implicit conversion: It requires the writing programmer to say “yeah, go ahead”, but it doesn’t help the reading programmer much. A call to truncate<int>(my_float) or round<int>(my_float) is much more expressive than a static_cast<int>(float), especially for user-defined types.

As such I give this goal:

Following Sean Parent, this is not a guideline because it is not always possible to follow it and it is not as bad, if you don’t.

Don’t use static_cast: Write your own functions to do static_cast conversions, truncate, round, to_underlying(my_enum) etc. and use those instead. This especially true for user-defined types, see below.

Again, a consequence of the goal is this guideline:

Don’t use explicit constructors to implement conversions (and don’t use explicit conversion operators).

Of course, absolutely do use explicit! Just not where you actually intend a usage of the form static_cast<T>(my_obj).

A notable exception to that rule is explicit operator bool: It basically provides the sane implicit conversions, so if (foo) and !foo works, but i + foo doesn’t.

Implementation of user-defined conversions

So if not using explicit constructors, how should you add new non-implicit conversions?

Well, use a function that takes an object of the source type and returns a new object of the destination type. A function has one big benefit over a constructor or conversion operator: It has a name.

As seen above, you can use that name to provide useful contextual information:

A bad name is static_cast<int>(my_float), a better name is gsl::narrow_cast<int>(my_float) — at least it informs that it is narrow, a good name is truncate<int>(my_float), because it also tells what it does in the error case.

The behavior in the error case is basically the only thing of a conversion function that is not obvious.

Note that a conversion function doesn’t need to have a prefix _cast. Only use it if there is no better name and/or it is a wide conversion where you don’t need to encode error information.

C++ Constructors

I have much more positive things to say about C++ constructors than C++ casts: After all, they’re the other half of the best feature in C++ — destructors.

So I’ll just repeat what others have said in this guideline:

Add a constructor to put an object in a valid, well-formed state: As such, it should take enough arguments to do that.

A “valid, well-formed state” is a state where the object is usable enough, you should be able to call the basic getter functions, for example.

However, this is just the bare minimum: You should also add other constructors to put the object in some convenient state.

Take this code, for example:

std::string str; // default constructor puts it into a well-formed state

// now set the actual contents
str = "Hello ";
str += std::to_string(42); // `std::to_string` is a cast, BTW

Something like this is definitely more convenient;

std::string str = "Hello " + std::to_string(42);

// str has the actual state already

However, following this to the extreme leads to something like this:

std::vector<int> vec(5, 2);

I actually don’t know what this does, I have to look it up every time.

Like with static_cast, there is no room to provide any additional information about the parameters. This is problem one with constructors.

The other one is this one: Suppose you’re creating some form of immutable object that needs to be initialized with a lot of state. You really shouldn’t pass in a ton of parameters to the constructor!

Only add constructors if the meaning of the parameters are clear and there aren’t too many parameters.

What should you do instead?

Well, there are two alternatives.

Named constructors

A named constructor is a free function or static member function that is used to construct the object. Again: you can give it a proper name!

For example, consider a file class. It has two main constructors: one that creates a new file and one that opens an existing one. However, both take just the file path, so it is even impossible to use constructors for it, as they cannot be overloaded!

But you can give them different names:

class file
{
public:
  static file open(const fs::path& p);
  static file create(const fs::path& p);
};



auto f1 = file::open();
auto f2 = file::create();

However, named constructors are not as ergonomic as regular constructors. You can’t use them with emplace(), for example.

A different implementation uses constructors and simply adds tags to give them names. Now they can be used with emplace like functions.

class file
{
public:
  static constexpr struct open_t {} open;
  file(open_t, const fs::path& p);

  static constexpr struct create_t {} create;
  file(create_t, const fs::path& p);
};



auto f1 = file(file::create, );
auto f2 = file(file::open, );

Which implementation of named constructor you use, is up to you. I tend to use the static function one more, but this is just my personal taste. You should definitely consider using one of both variants if you have complex constructors.

The builder pattern

If your constructors get too complex, the builder pattern helps. Instead of having just one creation function, you have an entire class: the builder. It contains many functions to set the different attributes and a finish() member function that returns the finalized object.

I use it for complex classes in cppast, because they are not mutable, so need to be completely created with all properties. Here is the cpp_class object, for example:

class cpp_class
{
public:
    class builder
    {
    public:
        // specify properties that always need to be provided
        explicit builder(std::string name, cpp_class_kind kind, bool is_final = false);

        // mark the class as final
        void is_final() noexcept;

        // add a base class
        cpp_base_class& base_class(std::string name, std::unique_ptr<cpp_type> type,
                                   cpp_access_specifier_kind access, bool is_virtual);


        // add a new access specifier
        void access_specifier(cpp_access_specifier_kind access);

        // add a child
        void add_child(std::unique_ptr<cpp_entity> child) noexcept;

        // returns the finished class
        std::unique_ptr<cpp_class> finish(const cpp_entity_index& idx, cpp_entity_id id,
                                          type_safe::optional<cpp_entity_ref> semantic_parent);

    private:
        std::unique_ptr<cpp_class> class_;
    };

     // but no public constructors
};

Note that the builder pattern has a couple of advantages over “inlining” the setter functions into the class:

One downside of the builder pattern is added verbosity. And if the created object is not polymorphic and returned by value, the nested class can’t simply have a member of the object is currently creating:

class foo
{
public:
    class builder
    {
        foo result_; // error: foo is an incomplete type at this point

        
    };

    
}:

To workaround that, either the builder must contain all members individually or must be defined outside the class:

class foo
{
public:
  class builder;

  
};

class foo::builder
{
  foo result_; // okay

  
};

But apart from those the builder pattern is a useful tool. However, it is only going to be used in rare situations.

Conclusion

When writing your own types, think about the constructors and cast operations you want to provide.

In particular:

Also try to avoid static_cast, use specialized casting functions instead. They’re more readable as they clearly show what is done.

Following this rules, you have interfaces that are easier to use and make it more obvious what they do.

This blog post was written for my old blog design and ported over. If there are any issues, please let me know.