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.

Monday, June 10, 2013

Blog Series: Effective compile-time assertions in C++ (1 of n)

This is the first of at least five blog posts covering the effective use of compile-time assertions in C++. Many thanks to those who have participated in reviewing this series: Tom Kirby-Green, @matt_dz, Marshall Clow, and James McNellis


The Basics

When implementing user-defined types or functions, there are assumptions that the developer must make about the input provided to the system or about the state of the system when the code is executed.

These assumptions fall into a few categories:

  • Assumptions that can be verified at compile-time
  • Assumptions that can be verified at run-time
  • Assumptions that cannot be verified
  • Unrecognized assumptions


Developers should prefer to verify as many assumptions as possible and to perform this verification as early in the development process as possible.

The C++ standard's run-time assertion capabilities are provided by the assert macro (from the <cassert> header).  It will turn into a no-op if the NDEBUG macro is defined (i.e. in "release" builds) when you include the header file.  You can use it like this.

#include <cassert>

const char * dayOfWeek (unsigned short which)
{
    assert(("There are only 7 days in a week", which <= 6));

    const char* days[] = { "Sunday", "Monday",
                           "Tuesday", "Wednesday",
                           "Thursday", "Friday",
                           "Saturday" };
    return days[which];
}

* There is a bit of a trick inside that assertion.  See herehere, and here for more information.

As of 2011, the C++ standard now includes a compile-time assertion facility that is built into the language, static_assert.  Unlike assert, static_assert never generates any code in the resulting binary, and thus cannot not be disabled.  Since it is a language keyword, it requires no header file.  It takes the following form.

static_assert ( constant-expression , string-literal ) ;

constant-expression must be convertible (at compile-time) to a bool.  If the value of constant-expression evaluates to true, then there are no side effects, otherwise the compiler will produce an error message that includes the value of string-literal.

static_assert(sizeof(char) <= sizeof(short), "Your compiler is no good."); should have no effect (unless you have a really REALLY bad compiler).  If you were to invert that comparison such that the assertion looked like static_assert(sizeof(char) > sizeof(short), "Your compiler is bad");, then you would get an error similar to this.

error: static assertion failed: Your compiler is bad

Those are the basics!

Availability

Unlike many other C++11 features, most shipping C++ compilers already implement static_assert, making it a great candidate for adoption into your coding practice.

Upcoming topics

I hope to get entries up for the rest of the series fairly soon.  Check back later for the rest of the topics in the series.
  • Monitoring external types for changes
  • Better template error messages
  • Future-proofing against buffer overflows
  • Future-proofing against unexpected overload resolution
  • Protecting against undesirable memory growth
  • Learn about the language and your compiler
  • TBD?
  • Possible futures for compile-time assertions