foonathan::blog()

Thoughts from a C++ library developer.

Controlling overload resolution #2: Improving error messages for failed overload resolution

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 second post shows you a simple way to improve the error messages when overload resolution fails and how to customize it completely.


Advertisement

Motivation

The first example has probably the longest error message you’ll encounter:

#include <iostream>
#include <string>

struct foo
{
    // ...
};

std::ostream& operator>>(std::ostream &os, const foo &f)
{
    // print f
    return os;
}

int main()
{
    foo f;
    std::cout << f;
}

The programmer has defined a user-defined type with something he thought was an output operator (or stream insertion operator, if you prefer). But instead of overloading operator<<, he made a typo and overloaded operator>>.

This happens to me all the time, I have to admit.

GCC generates an error message starting with:

main.cpp: In function ‘int main()’:
 main.cpp:18:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream {aka std::basic_ostream<char>}’ and ‘foo’)
    std::cout << f;
               ^
 In file included from /usr/include/c++/5.2.0/iostream:39:0,
                 from main.cpp:1:
 /usr/include/c++/5.2.0/ostream:628:5: note: candidate: std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&&, const _Tp&) [with _CharT = char; _Traits = std::char_traits<char>; _Tp = foo] <near match>
     operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
     ^
 /usr/include/c++/5.2.0/ostream:628:5: note:   conversion of argument 1 would be ill-formed:
 main.cpp:18:18: error: cannot bind ‘std::ostream {aka std::basic_ostream<char>}’ lvalue to ‘std::basic_ostream<char>&&’
     std::cout << f;
                  ^
 In file included from /usr/include/c++/5.2.0/iostream:39:0,
                 from main.cpp:1:
> /usr/include/c++/5.2.0/ostream:108:7: note: candidate: std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_ostream<_CharT, _Traits>::__ostream_type& (*)(std::basic_ostream<_CharT, _Traits>::__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; std::basic_ostream<_CharT, _Traits>::__ostream_type = std::basic_ostream<char>]
       operator<<(__ostream_type& (*__pf)(__ostream_type&))
       ^
 ....

The error message is followed by a list of all other candidates, 216 lines with a total of 17,686 characters! All because of a simple typo.

Another example

Let’s consider a simpler, shorter example I can extend without much difficulties.

You probably know - and hopefully not use! - the old C trick - or hack - to calculate the size of an array: sizeof(array) / sizeof(array[0]). It has a problem, though: array’s decay to pointers at almost every instance and even if you declare a function parameter as array, it is actually a pointer! This behavior was inherited from C.

C’s great.

So, if a naive programmer uses the array trick inside a function like so, he has a problem:

void func(int array[]) // actually a pointer, not an array!
{
    auto size = sizeof(array) / sizeof(array[0]); // actually: sizeof(int*) / sizeof(int)!
    ....
}

int main()
{
    int array[4];
    func(array); // array to pointer decay here
}

The code does not calculate the size of an array, it divides the size of a pointer by the size of an int. Unless on very strange systems, this is probably not 4.

So, what would a C++ programmer do?

He would use std::array. Or pass a range to the function.

But let’s ignore the (far better) alternatives.

A C++ programmer would write a function, let’s name it array_size, that calculates the size. C++ has templates, so there is no need to use the old sizeof “trick”:

template <typename T, std::size_t N>
constexpr std::size_t array_size(T(&)[N])
{
    return N:
}

If you have never seen T(&)[N], this is an (unnamed) reference to an array of N Ts. Yes, you can have references to array. With a name it would be T(&array)[N], but it is not needed here.

The syntax is great, isn’t it?

This function takes an array by reference and let template argument deduction figure out how big the array is.

Now, if the programmer would use array_size() instead of sizeof, he will get an error:

prog.cpp: In function 'void func(int*)':
prog.cpp:17:18: error: no matching function for call to 'array_size(int*&)'
  auto size = array_size(array);
                              ^
prog.cpp:4:23: note: candidate: template<class T, unsigned int N> constexpr std::size_t array_size(T (&)[N])
 constexpr std::size_t array_size(T(&)[N])
                       ^
prog.cpp:4:23: note:   template argument deduction/substitution failed:
prog.cpp:17:18: note:   mismatched types 'T [N]' and 'int*'
  auto size = array_size(array);
                              ^

We have transformed a runtime bug into a compile-time error. This is by far better, but the whole point of this post is to improve the error messages, so let’s do it.

Deleted fallback

In the previous post I have shown you how you can use = delete on arbitrary functions. If the function is overloaded, this will prevent calling it with the argument types in the deleted candidate.

This is exactly what we want!

If you pass anything but an array to array_size(), this shouldn’t list the base candidate. So we need a fallback function that is always valid. But this fallback should not exist, so we delete it.

For the small example with only one valid candidate, this doesn’t seem to be that useful. But bear with me for the rest of this paragraph.

But what is the argument of the fallback function? It must be able to take anything and must never be a better match than the valid function, otherwise the right types will go for the fallback.

If you are thinking “forwarding reference”, this is a bad idea. T&& is very easily a better match than anything else and will be prefered. Scott Meyers spents the whole item 26 on this topic in his book “Effective Modern C++”.

In this case, simply writing a template function taking a single argument by value is sufficient. An array type will always bind to the first overload, since it is more specialized, everything else to the by-value overload. So we declare this fallback overload and mark it as delete:

template <typename T, std::size_t N>
constexpr std::size_t array_size(T(&)[N])
{
    return N:
}

// deleted fallback overload
template <typename T>
constexpr std::size_t array_size(T) = delete;

The same call now results in:

 prog.cpp: In function 'void func(int*)':
 prog.cpp:20:30: error: use of deleted function 'constexpr std::size_t array_size(T) [with T = int*; std::size_t = unsigned int]'
  auto size = array_size(array);
                              ^
 prog.cpp:10:23: note: declared here
 constexpr std::size_t array_size(T) = delete;
                       ^

This may not seem as a big improvement to the previous array, but for a function with many valid overloads (like operator>>), this can be big, since the compiler will not list all the other candidates.

I could end the post right here, but I am not quite happy. The error message does not really give a reason why the overload resolution failed. Wouldn’t it be nice to give a complete customized error message instead?

Customized error message

What I’d like to have is a custom error message when the fallback is chosen. Custom error message sounds a lot like static_assert, so let’s try it:

template <typename T>
constexpr std::size_t array_size(T)
{
    static_assert(false, "array-to-pointer decay has occured, cannot give you the size");
    return 0; // to silence warnings
}

I’ve inserted a static_assert(false, ...) inside the function. This should trigger an error message when it is chosen by overload resolution.

Except that the static_assert is eager to mark the code as ill-formed; §14.6[temp.res]/8:

[…] If no valid specialization can be generated for a template, and that template is not instantiated, the template is ill-formed, no diagnostic required. […]

This bascially means, “as soon as you see that a template has invalid code, you may say so immediately”. And clang and GCC do it prior to instantiation and let the static_assert trigger immediately, while MSVC waits until instantiation.

So we need to force the compiler to evaluate the static_assert only when the template is actually instantiated. This can be done by making the boolean expression dependent on the template parameter. Then the compiler cannot evaluate the expression prior instantiation. The most common way to do is the following:

template <typename T>
constexpr std::size_t array_size(T)
{
    static_assert(sizeof(T) != sizeof(T), "array-to-pointer decay has occured, cannot give you the size");
    return 0; // to silence warnings
}

The size of T depends on the actual instantiated T, so it is only available after instantiation. This works, but I don’t find the solution very readable and a smart compiler could figure out that sizeof(T) is always equal to sizeof(T) and thus trigger the static_assert prior instantation.

So I suggest the following:

template <typename T>
struct not_an_array
{
    static constexpr bool error = false;
};

template <typename T>
constexpr std::size_t array_size(T)
{
    static_assert(not_an_array<T>::error, "array-to-pointer decay has occured, cannot give you the size");
    return 0; // to silence warnings
}

This works because not_an_array could have been specialized for certain types with a different value of the error constant. Using this fallback in the original code yields the following error message:

 prog.cpp: In instantiation of 'constexpr std::size_t array_size(T) [with T = int*; std::size_t = unsigned int]':
 prog.cpp:24:30:   required from here
 prog.cpp:18:5: error: static assertion failed: array-to-pointer decay has occured, cannot give you the size
     static_assert(not_an_array<T>::error, "array-to-pointer decay has occured, cannot give you the size");
     ^

This is a completely customized error message, which is what I wanted.

Note that this technique has a drawback: You then can’t use SFINAE to detect whether or not the call is well-formed, as the static_assert() doesn’t look into the function body.

SFINAE still works with = delete, it’s a shame you can’t give it a custom message. There is a proposal but it hasn’t been accepted as of yet.

Conclusion

If you call a function and overload resolution fails, the error messages are often very long listing all possible candidates. To avoid this, simply create a templated fallback overload that is selected as last resort. This fallback overload is either deleted or consists of a static_assert with a false boolean depending on the template parameter. The latter version allows for a completely customized error message.

It can be applied if you have a lot of overloads of a function and want a clear message when there is no possible candidate (like operator<<) or even when you have only a single function but want more information inside the error message, when it fails (like the array_size above).

In the next post of the series, I will show you a very powerful method to control exactly how to select an overload: tag dispatching.


Advertisement

This post was made possible by my Patreon supporters. If you'd like to support me as well, please head over to my Patreon and do so! One dollar per month can make all the difference.