Sunday, December 8, 2013

static_assert: Better template error messages

This is one in a series of blog posts covering the effective use of compile-time assertions in C++.


The Problem


Templates are one of the most powerful features of C++, and provide the language with a serious advantage over many of its brethren.  But templates have serious drawbacks as well, one of which is the incredibly verbose and dense error messages that are provided should you fail to provide the right kind of parameter to a templated entity.

Example 1:


#include <iostream>
#include <array>
#include <functional>
#include <vector>

using namespace std;

template <typename T, size_t N, typename F>
void fill_array (array<T, N> & a, F && f)
{
    for (auto & v : a)
        v = f();
}

int main ()
{
    array<vector<wstring>, 10> a;
    fill_array(a, []() { return vector<string>{"Goodbye"}; });
    return 0;
}

The problem with this code is that this instantiation of fill_array is expecting the lambda parameter to return a vector<wstring> instead of a vector<string>This (gcc 4.7.2) will generate the following compilation error message (I've included additional line breaks to help with the wrapping):

Output 1:


prog.cpp: In instantiation of ‘void fill_array(std::array<T, N>&, F&&) [with T = std::vector<std::basic_string<wchar_t> >; unsigned int N = 10u; F = main()::<lambda()>]’: prog.cpp:18:61: required from here prog.cpp:10:9: error: no match for ‘operator=’ in ‘v = main()::<lambda()>()’

prog.cpp:10:9: note: candidates are:
In file included from /usr/include/c++/4.7/vector:70:0,
                 from prog.cpp:4:

/usr/include/c++/4.7/bits/vector.tcc:161:5: note: std::vector<_Tp, _Alloc>& std::vector<_Tp, _Alloc>::operator=(const std::vector<_Tp, _Alloc>&) [with _Tp = std::basic_string<wchar_t>; _Alloc = std::allocator<std::basic_string<wchar_t> >]

/usr/include/c++/4.7/bits/vector.tcc:161:5: note: no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘const std::vector<std::basic_string<wchar_t> >&’

In file included from /usr/include/c++/4.7/vector:65:0,
                 from prog.cpp:4:

/usr/include/c++/4.7/bits/stl_vector.h:427:7: note: std::vector<_Tp, _Alloc>& std::vector<_Tp, _Alloc>::operator=(std::vector<_Tp, _Alloc>&&) [with _Tp = std::basic_string<wchar_t>; _Alloc = std::allocator<std::basic_string<wchar_t> >; std::vector<_Tp, _Alloc> = std::vector<std::basic_string<wchar_t> >]

/usr/include/c++/4.7/bits/stl_vector.h:427:7: note: no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘std::vector<std::basic_string<wchar_t> >&&’

/usr/include/c++/4.7/bits/stl_vector.h:449:7: note: std::vector<_Tp, _Alloc>& std::vector<_Tp, _Alloc>::operator=(std::initializer_list<_Tp>) [with _Tp = std::basic_string<wchar_t>; _Alloc = std::allocator<std::basic_string<wchar_t> >; std::vector<_Tp, _Alloc> = std::vector<std::basic_string<wchar_t> >]

/usr/include/c++/4.7/bits/stl_vector.h:449:7: note: no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘std::initializer_list<std::basic_string<wchar_t> >’

A Solution

You can think of compile-time assertions as extensions of the compiler. Since C++ is a general purpose programming language, it cannot inherently know much about the problem domain you are working with.  By using the utilities provided by <type_traits> along with static_assert, we can "teach" the compiler about the problem we are trying to solve, and thus provide a better error message for when the constraints of our domain are violated.


Example 2:


#include <iostream>
#include <array>
#include <functional>
#include <vector>
#include <type_traits>

using namespace std;

template <typename T, size_t N, typename F>
void fill_array (array<T, N> & a, F && f)
{
    static_assert(is_convertible<typename result_of<F()>::type, T>::value,
                  "MP-01: Incompatible type returned by f()");

    for (auto & v : a)
        v = f();
}

int main ()
{
    array<vector<wstring>, 10> a;
    fill_array(a, []() { return vector<string>{"Goodbye"}; });
    return 0;
}

Output 2 (note the text in blue):


prog.cpp: In instantiation of ‘void fill_array(std::array<T, N>&, F&&) [with T = std::vector<std::basic_string<wchar_t> >; unsigned int N = 10u; F = main()::<lambda()>]’:

prog.cpp:20:61: required from here
prog.cpp:10:5: error: static assertion failed: MP-01: Incompatible type returned by f()

prog.cpp:12:9: error: no match for ‘operator=’ in ‘v = main()::<lambda()>()’

prog.cpp:12:9: note: candidates are:
In file included from /usr/include/c++/4.7/vector:70:0,
                 from prog.cpp:4:

/usr/include/c++/4.7/bits/vector.tcc:161:5: note: std::vector<_Tp, _Alloc>& std::vector<_Tp, _Alloc>::operator=(const std::vector<_Tp, _Alloc>&) [with _Tp = std::basic_string<wchar_t>; _Alloc = std::allocator<std::basic_string<wchar_t> >]

/usr/include/c++/4.7/bits/vector.tcc:161:5: note: no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘const std::vector<std::basic_string<wchar_t> >&’

In file included from /usr/include/c++/4.7/vector:65:0,
                 from prog.cpp:4:

/usr/include/c++/4.7/bits/stl_vector.h:427:7: note: std::vector<_Tp, _Alloc>& std::vector<_Tp, _Alloc>::operator=(std::vector<_Tp, _Alloc>&&) [with _Tp = std::basic_string<wchar_t>; _Alloc = std::allocator<std::basic_string<wchar_t> >; std::vector<_Tp, _Alloc> = std::vector<std::basic_string<wchar_t> >]

/usr/include/c++/4.7/bits/stl_vector.h:427:7: note: no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘std::vector<std::basic_string<wchar_t> >&&’

/usr/include/c++/4.7/bits/stl_vector.h:449:7: note: std::vector<_Tp, _Alloc>& std::vector<_Tp, _Alloc>::operator=(std::initializer_list<_Tp>) [with _Tp = std::basic_string<wchar_t>; _Alloc = std::allocator<std::basic_string<wchar_t> >; std::vector<_Tp, _Alloc> = std::vector<std::basic_string<wchar_t> >]

/usr/include/c++/4.7/bits/stl_vector.h:449:7: note: no known conversion for argument 1 from ‘std::vector<std::basic_string<char> >’ to ‘std::initializer_list<std::basic_string<wchar_t> >’


Note that we still get all of the gross output in addition to the nice, clear error message.  I recommend that you put some special token (in the previous example, MP-01) in the error messages you provide to static_assert, so that you can cut through all of the garbage and go straight to the real problem(s).

Conclusion


I believe that the introduction of a language provided static_assert, will change the way that libraries are implemented and will help do a better job of enforcing API design decisions by the author(s) of the code at compile-time. Even better, until concepts (or something approaching them) have been approved, static_assert can fill in the gap rather nicely, as seen in this blog post from Eric Niebler.