How to make a safer C++ variant visitor, similar to switch statements?

Viewed 6312

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

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 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?

EDIT: s/implicit cast/implicit conversion/

EDIT2: 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.

3 Answers

Somewhat based on the visit_only_for example by Holt, I'm currently trying something like this to have a drop-in "tag" to my std::visit calls that prevents forgetting explicit handlers/operators:

//! struct visit_all_types_explicitly
//!
//! If this callable is used in the overload set for std::visit
//! its templated call operator will be bound to any type
//! that is not explicitly handled by a better match.
//! Since the instantiation of operator()<T> will trigger
//! a static_assert below, using this in std::visit forces
//! the user to handle all type cases.
//! Specifically, since the templated call operator is a
//! better match than call operators found via implicit argument
//! conversion, one is forced to implement all types even if
//! they are implicitly convertible without warning.
struct visit_all_types_explicitly {
    template<class> static inline constexpr bool always_false_v = false;

    // Note: Uses (T const&) instead of (T&&) because the const-ref version
    //       is a better "match" than the universal-ref version, thereby
    //       preventing the use of this in a context where another
    //       templated call operator is supplied.
    template<typename T>
    void operator()(T const& arg) const {
        static_assert(always_false_v<T>, "There are unbound type cases! [visit_all_types_explicitly]");
    }
};

using MyVariant = std::variant<int, double>;

void test_visit() {
    const MyVariant val1 = 42;

    // This compiles:
    std::visit(
        overloaded{
            kse::visit_all_types_explicitly(),
            [](double arg) {},
            [](int arg) {},
        },
        val1
        );

    // does not compile because missing int-operator causes
    // visit_all_types_explicitly::operator()<int> to be instantiated
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
        },
        val1
        );

    // does also not compile: (with static assert from visit_all_types_explicitly)
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
            [](auto&& arg) {}
        },
        val1
    );

    // does also not compile: (with std::visit not being able to match the overloads)
    std::visit(
        overloaded{
            visit_all_types_explicitly(),
            [](double arg) {},
            // [](int arg) {  },
            [](auto const& arg) {}
        },
        val1
    );
}

For now, this seems to do what I want, and what the OP asked for:

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.

You intentionally cannot combine this with an "default" / auto handler.

Related