如何在使用C++ std::visit时改善编译器错误信息?

10
我正在使用C++17的std::visit()函数处理一个有许多选项的variant,但是每当我在visitor中忘记一个或多个选项时,编译器产生的错误消息非常难以理解。
例如:
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
);
在上面的代码示例中,编译器可能会生成成千上万个字符的错误消息,这取决于备选方案的数量。有没有办法改进这些错误消息,以便编译器输出类似以下内容的信息?
example.cc:8-13: error: Non-exhaustive visitor -- missing alternative of type 'X'

1
除非编写自己的 visit 封装器,否则无法解决这个问题。但是尽管错误很可怕,但可能的原因并不太多:要么是缺少备选项,要么是返回类型不匹配。 - HolyBlackCat
6
很遗憾,不行。欢迎来到C++世界,在这里编译错误通常会比正在编译的代码还要长。 - Sam Varshavchik
1
晦涩难懂的错误信息长期以来一直困扰着C++。Bjarne多次提到了这个问题,并希望在C++中能够改进这个领域。 - Eljay
2
你可以使用编译器浏览器同时运行g++clang++MSVC。其中一个希望会给出一个可理解的错误信息 :-) - Ted Lyngmo
@HolyBlackCat 我该如何编写这个包装器?实现起来是否相当简单,还是说我基本上要重写 std::visit() - jinscoe123
4个回答

2

我第一次尝试解决这个问题的答案可以在这里找到。经过一些搜索和大量的试错,我提出了一个更好的解决方案,发布在这里。为了方便起见,我将下面复制粘贴解决方案。


这是一个概念证明。

#include <iostream>
#include <variant>


template <typename> class Test { };

using Foo = std::variant<
    Test<struct A>,
    Test<struct B>,
    Test<struct C>,
    Test<struct D>
    >;

using Bar = std::variant<
    Test<struct E>,
    Test<struct F>,
    Test<struct G>,
    Test<struct H>,
    Test<struct I>,
    Test<struct J>,
    Test<struct K>,
    Test<struct L>
    >;


template <typename T>
struct DefineVirtualFunctor
{
    virtual int operator()(T const&) const = 0;
};

template <template <typename> typename Modifier, typename... Rest>
struct ForEach { };
template <template <typename> typename Modifier, typename T, typename... Rest>
struct ForEach<Modifier, T, Rest...> : Modifier<T>, ForEach<Modifier, Rest...> { };

template <typename Variant>
struct Visitor;
template <typename... Alts>
struct Visitor<std::variant<Alts...>> : ForEach<DefineVirtualFunctor, Alts...> { };


struct FooVisitor final : Visitor<Foo>
{
    int operator()(Test<A> const&) const override { return  0; }
    int operator()(Test<B> const&) const override { return  1; }
    int operator()(Test<C> const&) const override { return  2; }
    int operator()(Test<D> const&) const override { return  3; }
};

struct BarVisitor final : Visitor<Bar>
{
    int operator()(Test<E> const&) const override { return  4; }
    int operator()(Test<F> const&) const override { return  5; }
    int operator()(Test<G> const&) const override { return  6; }
    int operator()(Test<H> const&) const override { return  7; }
    int operator()(Test<I> const&) const override { return  8; }
    int operator()(Test<J> const&) const override { return  9; }
    int operator()(Test<K> const&) const override { return 10; }
    int operator()(Test<L> const&) const override { return 11; }
};


int main(int argc, char const* argv[])
{
    Foo foo;
    Bar bar;
    
    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;
    default: foo = Foo{ std::in_place_index<3> }; break;
    }
    switch (argc) {
    case  0: bar = Bar{ std::in_place_index<0> }; break;
    case  1: bar = Bar{ std::in_place_index<1> }; break;
    case  2: bar = Bar{ std::in_place_index<2> }; break;
    case  3: bar = Bar{ std::in_place_index<3> }; break;
    case  4: bar = Bar{ std::in_place_index<4> }; break;
    case  5: bar = Bar{ std::in_place_index<5> }; break;
    case  6: bar = Bar{ std::in_place_index<6> }; break;
    default: bar = Bar{ std::in_place_index<7> }; break;
    }
    
    std::cout << std::visit(FooVisitor{ }, foo) << "\n";
    std::cout << std::visit(BarVisitor{ }, bar) << "\n";

    return 0;
}

正如您所看到的,Visitor类模板接受一个std::variant类型作为模板参数,从中定义一个接口,在任何继承自该模板类实例化的子类中必须实现该接口。如果在子类中忘记覆盖其中一个纯虚方法,则会出现以下错误。

$ g++ -std=c++17 -o example example.cc
example.cc: In function ‘int main(int, const char**)’:
example.cc:87:41: error: invalid cast to abstract class type ‘BarVisitor’
   87 |     std::cout << std::visit(BarVisitor{ }, bar) << "\n";
      |                                         ^
example.cc:51:8: note:   because the following virtual functions are pure within ‘BarVisitor’:
   51 | struct BarVisitor final : Visitor<Bar>
      |        ^~~~~~~~~~
example.cc:29:17: note:     ‘int DefineVirtualFunctor<T>::operator()(const T&) const [with T = Test<J>]’
   29 |     virtual int operator()(T const&) const = 0;
      |                 ^~~~~~~~

使用std::visit()时,编译器通常生成的错误消息比这要难懂得多。


1

我认为你可以将重载集合包装在一个函数对象中,该函数对象在未命中情况下执行默认例程(就像 switch 语句中的 default 部分一样)。并且我将默认重载放在开头,以免遗忘:

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
);

std::bind_back并不是一种神奇的方法,它只是提供了更加简洁的语法,无需使用占位符参数:

namespace par=std::placeholders;

std::visit(
    std::bind(
        any_visitor, par::_1//keep the front parameter
        [](auto&&){
            /* default visitor logic for missed cases*/
        },
        [](A const& a) { /* ... */ },
        [](B const& b) { /* ... */ },
        /* ... forgot 1+ alternatives ... */
    }),
    foo
);

很遗憾,我无法测试这个,因为我没有访问支持C++23的编译器,并且我的C++知识不足以将其转换为C++17。 - jinscoe123
@jinscoe123 这只是这个想法的一个示例。我编辑了答案,但仍然可能有人发现其中存在问题。基本思路是有条件地调用提供的重载或接受任何内容的回退默认重载。因为你不能简单地让通用重载与继承一起工作。你通过继承组合来捕获普通重载,并通过编译时条件(if constexpr)回退到通用默认值。 - Red.Wave

1
如果您能使用C++20,您可以使用概念。据我所知,改进模板代码中的错误消息是人们想要拥有此功能的关键原因之一。"简单地"用自己的visit函数包装std::visit,并添加一个requires子句即可:
#include <concepts>
#include <variant>

template <class Visitor, class... Ts>
requires (std::invocable<Visitor, Ts> && ...)
auto my_visit(Visitor&& vis, std::variant<Ts...> const& var) 
{
    return std::visit(std::forward<Visitor>(vis), var);
}

使用这个包装器可以将clang的错误信息从184行减少到28行。 实时演示(请注意,此实现仅涵盖const std::variant<...>&,一次仅处理一个变量,而std::visit可以同时处理任意数量的变量。)
不幸的是,它仍然没有你在问题中表达的简短和描述性,但我想它比原始版本好多了。
副笔:我无法告诉您为什么C++20的std::visit没有内置此requires子句。

1
不幸的是,我更喜欢等到C++20得到更广泛支持之后再使用它,但我点赞了你的答案,因为它是一个非常优雅的解决方案。不过,我迫不及待地想在不久的将来开始使用概念! :) - jinscoe123

0

我想出了一个不太理想的解决方案,但总比没有好。如果最终有更好的解决方案出现,我会很乐意将采纳答案切换到那个更好的方案。

这是一个概念验证。

#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
    );
}

当使用-fmax-errors=1(GCC)或-ferror-limit=1(Clang)进行编译时,STD_VISIT_IMPROVE_COMPILER_ERRORS_LAMBDA会导致静态断言消息打印出来,解释错误。然而,它并没有告诉我们哪个替代方案不满足要求,并且仍然无法防止原始、冗长和几乎难以理解的编译器错误被生成。至少,错误的原因更加清晰。

例如:

$ 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.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接