Home > Mobile >  C variant to variant with only some overlapping types, why doesn't my version work?
C variant to variant with only some overlapping types, why doesn't my version work?

Time:01-22

C 17, multiple compilers. TL;DR, my intended question is:

Why doesn't my solution work under gcc, but under clang and msvc ? Am I missing a subtlety about pack expansion or comma-separated expression evaluation?

My variation on this question appears to be at least somewhat unique, and in fact, I'm sure I could find a workaround to my problem, but I want to know why my attempts haven't been working.

I'm sure there are better ways to do this, but I'm looking at understanding the language.

Task:

Turn std::variant<A,B,C> v into std::variant<B,C,D> with the precondition that v has already had the case of it containing an A eliminated.

Turning one variant into another has all sorts of super interesting and educational answers on here, such as

Assign variant<A,B,C> from variant<C,B>?

and eventually I'll be digging into std::visit and its wonderful idioms.

Bootstrapping with:

struct Cat{};
struct Dog{};
struct Cow{};
struct Illithid{};


int main()
{

    using V = std::variant<Dog,Cat,Cow>;
    using Q = std::variant<Cat,Cow,Illithid>;
    V v = Cat{};
    auto q = transfer_variant<Q>(v);

    return 0;
}

I would expect q to be of type Q, storing a Cat.

My attempt went thusly:

template <typename R, typename ...Ts> R transfer_variant(const std::variant<Ts...>& v)
{

    R r;
    (
       ([](const auto& v, auto& r) {if constexpr (requires{R(std::get<Ts>(v)); }) { if (std::holds_alternative<Ts>(v)) { r = std::get<Ts>(v); } }}(v, r))
       , ...);

    return r;
}

R is the return variant type, and Ts denote the types stored in the source variant.

The basic idea is that I construct a default R, and then alter it provided I find that runtime type matches one of the passed in types. I use the idiom of parameter expansion in a comma-separated expression

((value),...)

to evaluate a series of immediately evaluated lambdas, where the return values are irrelevant and thrown out, but as a side effect, r gets altered just once.

The 'requires' clause is needed because one of the types in V is not in Q and I have to render that a null operation if it's not possible to do the assignment. It is not possible by the intended preconditions of the function for v to contain this invalid type, but the expansion generates the invalid expression anyway.

And so it works! Under clang and Visual Studio 2021, near trunk at the time of this post. It does not work under gcc, which gives:

<source>: In instantiation of 'R transfer_variant(const std::variant<_Types ...>&) [with R = std::variant<Cat, Cow, Illithid>; Ts = {Dog, Cat, Cow}]':
<source>:31:33:   required from here
<source>:12:49: error: parameter packs not expanded with '...':
   12 |     (([](const auto& v, auto& r) {if constexpr (requires{R(std::get<Ts>(v)); }) { if (std::holds_alternative<Ts>(v)) { r = std::get<Ts>(v); } }}(v, r)), ...);

So who's right here? clang and msvc do what I expect, law of least surprise, but that doesn't mean they've got the rules right.

(For those of you looking for an answer to what I went with, I settled on this while I'm learning about std::visit, and I'm still not quite sure how to get rid of the hokey null pointer cast to make the types work out, but this compiles in all three compilers:

template <typename R, typename ...Ts> R transfer_variant(const std::variant<Ts...>& v)
{
    R(*f[])(const std::variant<Ts...>&) = { [](const std::variant<Ts...>& v) {if constexpr (requires {R(std::get<Ts>(v)); }) { assert(false && "Unhandled type"); return *((R*)nullptr); } else { return R{}; } } ... };
    return f[v.index()](v);
}

...which builds a table of function pointers out of lambdas, and then calls just ONE of them based on the runtime index, but I still want to know whether I understand the language enough with regards to the original attempt)

CodePudding user response:

Why doesn't my solution work under gcc, but under clang and msvc ? Am I missing a subtlety about pack expansion or comma-separated expression evaluation?

GCC has some issues with the expansion of the parameter pack for the combination of template lambda and requires-clauses, so this is a bug of GCC.

CodePudding user response:

You can workaround this bug by using templated lambda:

template <typename R, typename... Ts>
R transfer_variant(const std::variant<Ts...>& v) {
    R r;
     ([]<typename T>(const auto& v, auto& r) {
        if constexpr (requires { R(std::get<T>(v)); }) {
            if (std::holds_alternative<T>(v)) {
                r = std::get<T>(v);
            }
        }}.template operator()<Ts>(v, r)
     , ...);

     return r;
}

This works in gcc.

BTW: I think this check:

if constexpr (requires { R(std::get<T>(v)); })

can be simplified to

if constexpr (std::is_constructible_v<R, T>)
  • Related