Disable Implicit Casts¶
Consider the following simple function:
void foo(uint32_t x);
The problem with foo
is that I can call it with anything which can be implicitly cast to a uint32_t
with no
complaints from the compiler.
It is perfectly valid to call foo
with a floating-point number: foo(5.9f)
.
The compiler will happily add an implicit cast to transform the argument to a 5
.
Most compilers have warnings that can be turned on to catch this sort of thing, but if you are a library writer or you
are working on a project where turning on these warnings would be a hassle, then -Wconversion
is not a reliable
option.
I want to opt-in to disabling implicit casts for a single function.
To do this for foo
, add the following declaration:
template <typename T>
void foo(T) = delete;
When the compiler sees a statement like foo(8.3)
, it searches for functions by name and it finds two valid
candidates for the name foo
: The templated one and the regular one.
Since we have more than one available function matching the name, we have to disambiguate by the argument types.
The definition template <typename T> foo
can be validly instantiated by the expression foo(8.3)
with
T=double
.
The instantiated function signature foo(double)
is a better match than foo(uint32_t)
, so the compiler decides
that you mean to call the templated function.
Now we ask the question: Is it legal to call this function?
That’s when <tt>= delete</tt> comes in – you can not call a deleted
function. Bam! No more implicit conversions allowed.</p>
Note
The question of “Is it legal to call this?” really is the last question the compiler asks. This can lead to far more surprising results in cases like this:
void bar(int x)
{ }
class base
{
private:
void bar() = delete;
};
class derived : public base
{
public:
derived()
{
bar(8);
}
};
At first glance, the expression bar(8)
in derived::derived
would call the free function which accepts an
integer, but it turns out only base::bar
is found in the name-matching stage.
The free function bar
we probably intended to call is never even evaluated for argument type match.
For more fun times with name lookups, check out the awesome article
Guru of the Week #30: Name Lookup.
Multiple Arguments¶
This same technique works for multiple argument functions, too:
void baz(size_t x, double y);
void baz(const std::string& s, const std::vector<uint32_t>& v);
template <typename T0, typename T1>
void baz(T0, T1) = delete;
This works for the same reasons the single-argument foo
works, except with two arguments.
An interesting question is: Why do you need both T0
and T1
?
Why not make the definition more like:
template <typename T>
void baz(T, T) = delete;
That definition says that for a template to match that pattern, both arguments must be the same; this can lead to
many unexpected consequences.
Consider the expression baz(5.6, 1)
.
The type for T
is ambiguous between double
and int
, which is a substitution failure
(not an error), so
template <typename T> baz
will not be matched.
This leaves baz(size_t, double)
and baz(const std::string&, const std::vector<uint32_t>&)
.
There is an implicit conversion of 5.6
to a size_t
and of 1
to a double
, so baz(size_t, double)
is
selected.
The lesson?
Use a separate template parameter for each function argument you want to disable implicit casting for.
So what if you have arguments where you do not care if there is an implicit cast? You can do that, too!
void fizz(uint32_t x, const std::string& s);
template <typename T>
void fizz(T, const std::string&) = delete;
Swashbuckling with Variadic Templates¶
Let’s say I have a whole host of functions:
void* buzz(const something& s);
double buzz(int, double, float, long long);
int buzz(const char*, int);
long long buzz(unsigned int, long long);
Yuck!
All these functions have different return types and different numbers of arguments (arity).
I do not want any implicit conversions happening on any of them.
Now, I could use the template trick for each arity of buzz
, but there are two major problems with doing that.
If someone comes in and adds a function of arity 3 (say buzz(int, double, void*)
), we will not have a function
deletion covering it.
More importantly, writing = delete
so frequently is a lot of work (and a good C++ pirate is lazy).
Here, variadic templates come to the rescue!
template <typename... T>
void buzz(T...) = delete;
Now, any attempted call to a function named buzz
which does not exactly match a non-template option will show up as
a deleted function.
You probably noticed that we do not care about the return type at all.
This means that expressions needing a specific return type from the result will fail, but that does not matter, since
the expression is invalid anyway due to the = delete
business.
That means we can wrap this whole deal up in a quite nice-looking macro:
#define DISABLE_IMPLICIT_CASTS(func_name) \
template <typename... T> \
void func_name(T...) = delete
Then again, the = delete
looks fine to me.