Home > Software engineering >  How to improve compiler error messages when using C std::visit?
How to improve compiler error messages when using C std::visit?

Time:06-06

I am using C 17's std::visit() function on a variant with many alternatives, and the error messages produced by the compiler whenever I forget one or more of the alternatives in my visitor are quite difficult to understand.

e.g.

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

using Foo = std::variant<A, B, /* ... many more alternatives ... */>;

Foo foo;

std::visit(overloaded{
    [](A const& a) { /* ... */ },
    [](B const& b) { /* ... */ },
    /* ... forgot 1  alternatives ... */
    }, foo
);

In the above code example, the compiler can produce error messages that are thousands of characters in length, depending on the number of alternatives. Is there a way to improve these error messages so that the compiler will output something like the following instead?

example.cc:8-13: error: Non-exhaustive visitor -- missing alternative of type 'X'

CodePudding user response:

I've come up with a less-than-ideal solution, but it's better than nothing. If a better solution eventually comes along, I will happily switch the accepted answer to that.

Here's a proof of concept.

#include <variant>

#define STD_VISIT_IMPROVE_COMPILER_ERRORS_LAMBDA \
[](auto... __args) { \
    static_assert(always_false_v<decltype(__args)...>, "non-exhaustive visitor"); \
},

template <typename... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <typename... Ts> overloaded(Ts...) -> overloaded<Ts...>;

template <typename> constexpr bool always_false_v = false;

template <typename> class Test { };

using Foo = std::variant<
    std::monostate,
    Test<struct A>,
    Test<struct B>,
    Test<struct C>,
    Test<struct D>,
    Test<struct E>,
    Test<struct F>,
    Test<struct G>,
    Test<struct H>,
    Test<struct I>,
    Test<struct J>,
    Test<struct K>,
    Test<struct L>,
    Test<struct M>,
    Test<struct N>,
    Test<struct O>,
    Test<struct P>,
    Test<struct Q>,
    Test<struct R>,
    Test<struct S>,
    Test<struct T>,
    Test<struct U>,
    Test<struct V>,
    Test<struct W>,
    Test<struct X>,
    Test<struct Y>,
    Test<struct Z>
    >;

int main(int argc, char const* argv[])
{
    Foo foo;

    switch (argc) {
    case  0: foo = Foo{ std::in_place_index< 0> }; break;
    case  1: foo = Foo{ std::in_place_index< 1> }; break;
    case  2: foo = Foo{ std::in_place_index< 2> }; break;
    case  3: foo = Foo{ std::in_place_index< 3> }; break;
    case  4: foo = Foo{ std::in_place_index< 4> }; break;
    case  5: foo = Foo{ std::in_place_index< 5> }; break;
    case  6: foo = Foo{ std::in_place_index< 6> }; break;
    case  7: foo = Foo{ std::in_place_index< 7> }; break;
    case  8: foo = Foo{ std::in_place_index< 8> }; break;
    case  9: foo = Foo{ std::in_place_index< 9> }; break;
    case 10: foo = Foo{ std::in_place_index<10> }; break;
    case 11: foo = Foo{ std::in_place_index<11> }; break;
    case 12: foo = Foo{ std::in_place_index<12> }; break;
    case 13: foo = Foo{ std::in_place_index<13> }; break;
    case 14: foo = Foo{ std::in_place_index<14> }; break;
    case 15: foo = Foo{ std::in_place_index<15> }; break;
    case 16: foo = Foo{ std::in_place_index<16> }; break;
    case 17: foo = Foo{ std::in_place_index<17> }; break;
    case 18: foo = Foo{ std::in_place_index<18> }; break;
    case 19: foo = Foo{ std::in_place_index<19> }; break;
    case 20: foo = Foo{ std::in_place_index<20> }; break;
    case 21: foo = Foo{ std::in_place_index<21> }; break;
    case 22: foo = Foo{ std::in_place_index<22> }; break;
    case 23: foo = Foo{ std::in_place_index<23> }; break;
    case 24: foo = Foo{ std::in_place_index<24> }; break;
    case 25: foo = Foo{ std::in_place_index<25> }; break;
    default: foo = Foo{ std::in_place_index<26> }; break;
    }

    return std::visit(overloaded{
        [](std::monostate) { return  0; },
        [](Test<A> const&) { return  1; },
        [](Test<B> const&) { return  2; },
        [](Test<C> const&) { return  3; },
        [](Test<D> const&) { return  4; },
        [](Test<E> const&) { return  5; },
        [](Test<F> const&) { return  6; },
        [](Test<G> const&) { return  7; },
        [](Test<H> const&) { return  8; },
        [](Test<I> const&) { return  9; },
        [](Test<J> const&) { return 10; },
        [](Test<K> const&) { return 11; },
        [](Test<L> const&) { return 12; },
        [](Test<M> const&) { return 13; },
        [](Test<N> const&) { return 14; },
        [](Test<O> const&) { return 15; },
        [](Test<P> const&) { return 16; },
        [](Test<Q> const&) { return 17; },
        [](Test<R> const&) { return 18; },
        [](Test<S> const&) { return 19; },
        [](Test<T> const&) { return 20; },
        [](Test<U> const&) { return 21; },
        [](Test<V> const&) { return 22; },
        [](Test<W> const&) { return 23; },
//      [](Test<X> const&) { return 24; },  // Whoops...
        [](Test<Y> const&) { return 25; },
        [](Test<Z> const&) { return 26; },
        STD_VISIT_IMPROVE_COMPILER_ERRORS_LAMBDA
        }, foo
    );
}

When compiling with -fmax-errors=1 (GCC) or -ferror-limit=1 (Clang), the STD_VISIT_IMPROVE_COMPILER_ERRORS_LAMBDA causes the static assert message to print out, explaining the error. Unfortunately, however, it does not tell us which alternative is unsatisfied and it still does not prevent the original, long and practically-unintelligible compiler error from being generated. At least, though, the cause of the error is more clear.

e.g.

$ g   -std=c  17 -fmax-errors=1 -o example example.cc
...
example.cc:5:19: error: static assertion failed: non-exhaustive visitor
    5 |     static_assert(always_false_v<decltype(__args)...>, "non-exhaustive visitor"); \
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
example.cc:107:9: note: in expansion of macro ‘STD_VISIT_IMPROVE_COMPILER_ERRORS_LAMBDA’
  107 |         STD_VISIT_IMPROVE_COMPILER_ERRORS_LAMBDA
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated due to -fmax-errors=1.

CodePudding user response:

IMO you can wrap the overload set in a function object that executes a default routine on missed cases (much like a default section in a switch statement). And I put the default overload at the beginning, so as to not forget it:

auto any_visitor=[](auto&& val, auto&& default_fn,auto ...fn){
    overloaded vis{fn ...};
    if constexpr(std::is_invokable_v<decltype(vis), decltype(val)>)
        return vis(std::forward(val));
    else
        return std::invoke(std::forward(default_fn), std::forward(val));
};

std::visit(
    std::bind_back(
        any_visitor,
        [](auto&&){
            /* default visitor logic for missed cases*/
        },
        [](A const& a) { /* ... */ },
        [](B const& b) { /* ... */ },
        /* ... forgot 1  alternatives ... */
    }),
    foo
);
  • Related