Thoughts from a C++ library developer.

Controlling overload resolution #1: Preventing implicit conversions

Overload resolution is one of C++ most complicated things and yet it works most of the time without the need to think about it.
In this mini-series, I will show you how to control this complex machinery so it is even more powerful and completely under your control.

The first post shows you how to delete candidates and how you can use that to prevent implicit conversions.

C++11’s =delete

Most of you know that since C++11 you can specify = delete to inhibit the generation of the special member functions like copy or move constructors.
But less people know that you can use it on any function and delete it.

The standard simply specifies in the beginning of §8.4.3[dcl.fct.def.delete]:

1 A function definition of the form: attribute-specifier-seqopt decl-specifier-seqopt declarator virt-specifier-seqopt = delete ; is called a deleted definition. A function with a deleted definition is also called a deleted function.

2 A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed.

Now we have two versions of func, one taking an int and a deleted one taking a double.
On the first look it does not seem any more useful than before.
If you don’t want to have an overload, simply don’t declare it!

But take a second look and consider the consequences of §8.4.3:
A function with = delete at the end, isn’t only a declaration, it is also a definition!
And since name lookup only looks for matching declarations, a deleted function is a normal candidate that can participate in overload resolution.

But the compiler can see that the function has a deleted definition, so why is it considered?

Because it is completely legal to declare a function in a header file and delete it in the source file, since a deleted function provides a normal definition.

(It is not, my mistake).
And also because the standard says so.™

If you write func(5.0), you now call it with a double.
The compiler chooses the overload for double, because a deleted function participates in overload resolution, and complains that the function is deleted.

This prohibits passing double to func, even though it could be implictly converted.

Prohibiting implicit conversions

As shown above, you can delete candidates to avoid certain implicit conversions in overload resolution.

If you have one or more overloads of a function accepting a certain set of types,
you can also call it with types that are implictly convertible to the accepted types.
Often this is great and terse and avoids verbose boilerplate.

But sometimes these implicit conversion are not without loss or expensive.
User-defined conversions can be controlled by using explicit,
but the implicit conversions built-in in the language like double to int?
You can’t write explicit there.

Allowing lossy conversions was one of the many mistakes C made and C++ inherited.

But you can write another overload that takes the types you want to prohibit and delete it.

Let’s extend the example above by prohibiting all floating points, not only double:

You should write an overload for each floating point type,
otherwise the call is ambigous.
E.g. when having only the deleteddouble overload,
calling it with a long double is ambigous between int and double,
since both conversions lead to a loss.
It works here but could have side-effects in more complicated examples.
So to be safe, just write it explicitly.

You could also use templates to generate the three overloads,
use SFINAE to enable it only for floating points:

Probihiting implicit conversions: Temporaries

Some kind of implicit conversions can be especially bad:
Those user-defined conversions that create temporaries.

For example, passing a string literal to a function taking a std::string creates a temporary std::string to initialize the argument.
This can be especially surprising in the following case:

voidfunc(conststd::string&str);...func("Hello, this creates a temporary!");

Here the writer of func took a std::string by (const) reference because he or she doesn’t want to copy the string, because that can involve costly heap allocations.
But passing a string literal does involve heap allocations due to the temporary.
And since temporary (rvalues, that is) bind to const (lvalue) references, this works.

This is often behavior that is tolerated, but sometimes the cost can be too expensive to allow the (accidental) creation of the temporary.
In this case, a new overload can be introduced that takes a const char*, which is deleted:

voidfunc(conststd::string&str);voidfunc(constchar*)=delete;...func("this won't compile");func(std::string("you have to be explicit"));

On a related note, sometimes you have a function taking a const reference to something and the function stores a pointer to it somewhere.
Calling it with a temporary would not only be expensive, but fatal, since the temporary is - well - temporary and the pointer will soon point to a destroyed object:

Here in this case we need the more general form of disallowing any temporary objects.
So we need an overload taking any rvalue, that is an overload taking an rvalue reference:

voidfunc(constT&obj){...}voidfunc(T&&)=delete;...func(T());// does not compile

Note that T is a concrete type here, the deleted overload is no template and thus T&& is not a forwarding reference, but a normal rvalue reference.

This works, but it isn’t perfect.
Let’s say you have a function foo that returns a const T (for some reason):

constTfoo();voidfunc(constT&obj){...}voidfunc(T&&)=delete;...func(foo());// does compile!

This compiles because a const rvalue does not bind to a non-const rvalue reference,
as such the lvalue overload is selected, which is - again - dangerous.

The solution? Simple, just use a const rvalue reference:

constTfoo();voidfunc(constT&obj){...}voidfunc(constT&&)=delete;...func(foo());// does not compile

The deleted overload accepts any rvalue, const or non-const.
This is one of the few good use cases for const rvalue references.

This trick is also used in the standard library.
func is std::ref/std::cref, the temporary versions are deleted,
so that no std::reference_wrapper to a temporary is created.

Conclusion

Sometimes it can be useful to forbid certain kinds of implicit conversions in function overloading,
since they can be expensive or lead to loss.

This is especially true for temporaries that bind to const lvalue referenceres.
They can also be dangerous, if you take and store an address of the referenced object,
then you don’t want to allow temporaries as arguments.

To prevent such things, simply define new overloads that take the type which would be implictly converted
and mark it as deleted.
In the case of preventing temporaries, the new overload should take a const rvalue reference to the appropriate type.

Overload resolution will prefer an exact match and choose the deleted overload
which result in a compile-time error.

In the next post of this mini series, I will use this technique even further to improve error messages on failed overload resolution
and show you a way to completely customize the error message when a deleted function is chosen.