1

Consider the following code:

#include <iostream>
#include <variant>
#include <memory>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;

struct foo {
    int f;
    foo(int n) : f(n) {}
};

struct bar {
    std::string b;
};

using unflattened_variant = std::variant<int, std::string, std::unique_ptr<foo>, std::unique_ptr<bar>>;
using flattened_variant = std::variant<int, std::string, foo, bar>;

flattened_variant flatten(const unflattened_variant& v) {
    return std::visit(
        overloaded{
            [](int v) -> flattened_variant {
                return v;
            },
            [](const std::string& s) -> flattened_variant {
                return s;
            },
            [](const std::unique_ptr<foo>& f) -> flattened_variant {
                return *f;
            },
            [](const std::unique_ptr<bar>& b) ->  flattened_variant {
                return *b;
            },
        },
        v
    );
}

int main()
{
    unflattened_variant uv{ std::make_unique<foo>(42) };
    auto fv = flatten(uv);
    std::cout << std::get<foo>(fv).f << "\n";
}

This is a toy example that illustrates a situation I am running into in real code. I want to simplify the implementation of flatten(...) such that it is less verbose when there are more types in the variant.

Basically the situation is, I have a variant that contains some simple types and some move-only types that I would like to do something with. The operation that I need to perform is the same for all simple types and the same for all the move-only types; however, I can't think of a way of dealing with the two cases (simple or move-only) using only two visiting functions. e.g. this is illegal C++ but illustrates what I want to do

flattened_variant flatten(const unflattened_variant& v) {
    return std::visit(
        overloaded{
            [](const std::unique_ptr<auto>& u_ptr) -> flattened_variant {
                return *u_ptr;
            },
            [](auto simple_value) ->  flattened_variant {
                return simple_value;
            },
        },
        v
    );
}

I have dealt with situations like this in the past by using a custom variant cast, similar to the one implemented here, to cast to a variant containing just those types that need to be handled the same and then using a lambda taking an auto parameter as the visitor; however, such a cast would not work in this case because you can't copy unique_ptrs and you can't make a variant containing references. I suppose I could write a function that will cast to a variant of pointers but am wondering if there is an easier way.

cigien
  • 57,834
  • 11
  • 73
  • 112
jwezorek
  • 8,592
  • 1
  • 29
  • 46

1 Answers1

3
template<template<class...>class, class> struct is_instance_of:std::false_type{};
template<template<class...>class Z, class...Ts> struct is_instance_of<Z,Z<Ts...>>:std::true_type{};

template<template<class...>class Z, class T>
constexpr bool is_instance_of_v=is_instance_of<Z,T>::value;

flattened_variant flatten(unflattened_variant const& v) {
  return std::visit([](auto const& e)->flattened_variant{
    using T = std::decay_t<decltype(e)>;
    if constexpr (is_instance_of_v<std::unique_ptr, T>){
      return *e;
    else
      return e;
  }, v);
}

we add a trait to dispatch on, then use if constexpr to have 2 function bodies.

In we have lots more options.

[]<class T>(T const& e)->flattened_variant{
  if constexpr (is_instance_of_v<std::unique_ptr, T>){

Then, going back to overloading solution, we have:

[]<class T>(std::unique_ptr<T> const&)

or

template<class T, template<class...>class Z>
concept instance_of=is_instance_of<Z,T>::value;

then

[](instance_of<std::unique_ptr> auto const& e)

or

[]<<instance_of<std::unique_ptr> T>(T const& e)

Prior to in we can use a dispatch helper:

template<class T0, class T1>
constexpr T0&& constexpr_branch( std::true_type, T0&& t0, T1&& ) { return std::forward<T0>(t0); }
template<class T0, class T1>
constexpr T1&& constexpr_branch( std::false_type, T0&&, T1&& t1 ) { return std::forward<T1>(t1); }

flattened_variant flatten(unflattened_variant const& v) {
  return std::visit([](auto const& e)->flattened_variant{
    using T = std::decay_t<decltype(e)>;
    return constexpr_branch(
      is_instance_of<std::unique_ptr, T>, 
      [](auto const& e){return *e;},
      [](auto const& e){return e;}
    )(e);
  }, v);
}

going back to (where did you get your variant?), you could make an external class:

template<class R>
struct flatten {
  template<class T>
  R operator()(std::unique_ptr<T> const& p)const{
    return *p;
  }
  template<class T>
  R operator()(T const& v)const{
    return v;
  }
};

then just do a

return std::visit( flatten<flattened_variant>{}, v );
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524