如何制作更安全的 C++ 变体访问者,类似于 switch 语句?

很多人使用 C++17/boost 变体的模式看起来与 switch 语句非常相似.例如:(来自 cppreference.com 的片段)

The pattern that a lot of people use with C++17 / boost variants looks very similar to switch statements. For example: (snippet from cppreference.com)

std::variant<int, long, double, std::string> v = ...;

std::visit(overloaded {
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);

问题是当您在访问者中输入错误类型或更改变体签名时,却忘记更改访问者.您将调用错误的 lambda,通常是默认的,而不是得到编译错误,或者您可能会得到一个您没有计划的隐式转换.例如:

The problem is when you put the wrong type in the visitor or change the variant signature, but forget to change the visitor. Instead of getting a compile error, you will have the wrong lambda called, usually the default one, or you might get an implicit conversion that you didn't plan. For example:

v = 2.2;
std::visit(overloaded {
    [](auto arg) { std::cout << arg << ' '; },
    [](float arg) { std::cout << std::fixed << arg << ' '; } // oops, this won't be called
}, v);

枚举类上的 Switch 语句更加安全,因为您不能使用不属于枚举的值来编写 case 语句.同样,我认为如果变体访问者仅限于变体中包含的类型的子集以及默认处理程序,那将非常有用.有没有可能实现类似的东西?

Switch statements on enum classes are way more secure, because you can't write a case statement using a value that isn't part of the enum. Similarly, I think it would be very useful if a variant visitor was limited to a subset of the types held in the variant, plus a default handler. Is it possible to implement something like that?

s/隐式转换/隐式转换/

s/implicit cast/implicit conversion/

我想要一个有意义的包罗万象的 [](auto) 处理程序.我知道如果您不处理变体中的每种类型,删除它会导致编译错误,但这也会从访问者模式中删除功能.

I would like to have a meaningful catch-all [](auto) handler. I know that removing it will cause compile errors if you don't handle every type in the variant, but that also removes functionality from the visitor pattern.

推荐答案

如果您只想允许类型的子集,那么您可以在 lambda 的开头使用 static_assert,例如:

If you want to only allow a subset of types, then you can use a static_assert at the beginning of the lambda, e.g.:

template <typename T, typename... Args>
struct is_one_of: 
    std::disjunction<std::is_same<std::decay_t<T>, Args>...> {};

std::visit([](auto&& arg) {
    static_assert(is_one_of<decltype(arg), 
                            int, long, double, std::string>{}, "Non matching type.");
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int with value " << arg << '
';
    else if constexpr (std::is_same_v<T, double>)
        std::cout << "double with value " << arg << '
';
    else 
        std::cout << "default with value " << arg << '
';
}, v);

如果您在变体中添加或更改类型,或者添加一个类型,这将失败,因为 T 需要完全是给定类型之一.

This will fails if you add or change a type in the variant, or add one, because T needs to be exactly one of the given types.

您也可以使用您的 std::visit 变体,例如带有默认"访问者,例如:

You can also play with your variant of std::visit, e.g. with a "default" visitor like:

template <typename... Args>
struct visit_only_for {
    // delete templated call operator
    template <typename T>
    std::enable_if_t<!is_one_of<T, Args...>{}> operator()(T&&) const = delete;
};

// then
std::visit(overloaded {
    visit_only_for<int, long, double, std::string>{}, // here
    [](auto arg) { std::cout << arg << ' '; },
    [](double arg) { std::cout << std::fixed << arg << ' '; },
    [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);

如果您添加的类型不是 intlongdoublestd::string,然后 visit_only_for 调用运算符将??匹配,您将有一个模棱两可的调用(在此调用和默认调用之间).

If you add a type that is not one of int, long, double or std::string, then the visit_only_for call operator will be matching and you will have an ambiguous call (between this one and the default one).

这也应该在没有默认值的情况下工作,因为 visit_only_for 调用运算符将??匹配,但由于它被删除,你会得到一个编译时错误.

This should also works without default because the visit_only_for call operator will be match, but since it is deleted, you'll get a compile-time error.

相关文章