Incomplete type `std::variant<...>` used in nested name specifier

Viewed 495

I wrote the following code into a file named main.cpp. It involves the curiously recurring template pattern (CRTP) with the standard type std::variant.

#include <string>
#include <variant>
#include <vector>

template<typename T>
struct either {
    std::vector<T> arg;
};

template<typename T>
struct maybe_either: std::variant<T, either<maybe_either<T>>> {

    template<typename U>
    maybe_either(U&& v):
      std::variant<T, either<maybe_either<T>>>(std::forward<U>(v)) {
    }
};

struct var {
  std::string name;
};

int main(int, char**) {
    auto expression = maybe_either<var>(either<maybe_either<var>>{});
    std::visit([&](auto&& v) {
        using T = std::decay_t<decltype (v)>;
        if constexpr (std::is_same_v<T, var>) {
          // ...
        } else if constexpr (std::is_same_v<T, either<maybe_either<var>>>) {
          // ...
        }
    }, expression);
    return 0;
}

When compiling it with the following command line, I get the error message below:

$ g++ -c -std=c++17 main.cpp
In file included from main.cpp:2:0:
/usr/include/c++/7/variant: In instantiation of ‘constexpr const size_t std::variant_size_v<maybe_either<var> >’:
/usr/include/c++/7/variant:702:10:   required from ‘struct std::__detail::__variant::__gen_vtable<void, main(int, char**)::<lambda(auto:1&&)>&&, maybe_either<var>&>’
/usr/include/c++/7/variant:1255:23:   required from ‘constexpr decltype(auto) std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = main(int, char**)::<lambda(auto:1&&)>; _Variants = {maybe_either<var>&}]’
main.cpp:32:18:   required from here
/usr/include/c++/7/variant:97:29: error: incomplete type ‘std::variant_size<maybe_either<var> >’ used in nested name specifier
     inline constexpr size_t variant_size_v = variant_size<_Variant>::value;
                             ^~~~~~~~~~~~~~
/usr/include/c++/7/variant: In instantiation of ‘constexpr const auto std::__detail::__variant::__gen_vtable<void, main(int, char**)::<lambda(auto:1&&)>&&, maybe_either<var>&>::_S_vtable’:
/usr/include/c++/7/variant:711:29:   required from ‘struct std::__detail::__variant::__gen_vtable<void, main(int, char**)::<lambda(auto:1&&)>&&, maybe_either<var>&>’
/usr/include/c++/7/variant:1255:23:   required from ‘constexpr decltype(auto) std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = main(int, char**)::<lambda(auto:1&&)>; _Variants = {maybe_either<var>&}]’
main.cpp:32:18:   required from here
/usr/include/c++/7/variant:711:49: error: ‘_S_apply’ was not declared in this scope
       static constexpr auto _S_vtable = _S_apply();
                                         ~~~~~~~~^~

My class maybe_either derived from std::variant<...> can be used normally in other contexts, but when I call std::visit(...) on it, it fails to compile. What is wrong?

1 Answers

This is basically LWG3052 which I'm trying to address in P2162.

maybe_either<T> isn't a specialization of std::variant - it inherits from one. And std::visit is currently underspecified. It's not at all clear what kinds of "variants" are allowed to be visited.

libstdc++ implements the original suggested direction in that library issue, which is only specializations of std::variant (of which you are not). libc++, on the other hand, allows types that inherit from std::variant to be visited, so it accepts your example.

The intent is that the example as-is will become well-formed eventually. But until then, you'll have to ensure that the visit you do is directly on a std::variant. You can do so by adding your own member or non-member visit that does this cast for you, so the callers don't have to do it themselves.

For example, this:

template<typename T>
struct maybe_either: std::variant<T, either<maybe_either<T>>> {
    using base = typename maybe_either::variant;

    template<typename U>
    maybe_either(U&& v):
      std::variant<T, either<maybe_either<T>>>(std::forward<U>(v)) {
    }

    template <typename F>
    decltype(auto) visit(F&& f) & {
        return std::visit(std::forward<F>(f), static_cast<base&>(*this));
    }
};

allows this to work:

int main(int, char**) {
    auto expression = maybe_either<var>(either<maybe_either<var>>{});
    expression.visit([&](auto&& v) {
        using T = std::decay_t<decltype (v)>;
        if constexpr (std::is_same_v<T, var>) {
          // ...
        } else if constexpr (std::is_same_v<T, either<maybe_either<var>>>) {
          // ...
        }
    });
    return 0;
}
Related