4

I have this type:

struct immobile {
   // other stuff omitted
   immobile(immobile&) = delete;
   immobile(immobile&&) = delete;
};
immobile mk_immobile();
// e.g. this compiles
// mk_immobile() is a prvalue and i is its result object
immobile i(mk_immobile());

I also have this class template:

template<typename T>
struct container {
    std::variant<T, other_stuff> var;
    template<typename... Args>
    container(Args&&... args)
    : var(std::in_place_index<0>, std::forward<Args>(args)...) {}
};

I want to construct a container around the object produced by mk_immobile(), with the immobile object used to initialize one of the variants of var.

container<immobile> c(mk_immobile());

However, this does not work. For one, std::variant's constructor wants std::is_constructible_v<immobile, immobile>, which doesn't hold. Worse, even this simplified version fails:

template<typename T>
struct demonstration {
    T t;
    template<typename... Args>
    demonstration(Args&&... args) : t(std::forward<Args>(args)...) {}
};
demonstration<immobile> d(mk_immobile());

Which seems to imply that std::forward does not, in fact, perfectly forward—prvalues do not forward as prvalues. (This makes sense to me; I don't think doing that would be possible.) I can make demonstration work by changing it to this:

template<typename T>
struct demonstration {
    T t;
    template<typename F>
    demonstration(F&& f) : t(std::forward<F>(f)()) {}
};
demonstration<immobile> d([] { return mk_immobile(); });

But I do not see a way to change container in a similar manner. How do I change container so that it can construct a std::variant (or other tagged union) out of a prvalue? I can change container but cannot change immobile.

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • From a quick test: https://gcc.godbolt.org/z/27cALe It seems like it should be possible to implement a tagged union that behaves the way you want, by using your defered callback-based construction idea (I just did a much simpler version of the same thing) –  Jun 04 '19 at 02:42

1 Answers1

3

You abuse casts

template<typename F>
struct initializer
{
    F f;
    template<typename T>
    operator T()
    {
        return f();
    }
};

template<typename F>
initializer(F&&) -> initializer<F>;

And use as

container<immobile> c{initializer{[]{
    return mk_immobile();
}}};
Passer By
  • 19,325
  • 6
  • 49
  • 96
  • I like it! However, I think `initializer` might need two changes to make it forward more perfectly. I think `F f` should be `F&& f` and `f()` should be `std::forward(f)()`. Functors can use perfect forwarding because `operator()` can be present in both `&`-qualified and `&&`-qualified versions, and forwarding allows more fitting overload resolution. `initializer`s are only meant to be used once, so I think it's safe to allow the functor to move itself away. See [this example](https://www.godbolt.org/z/rWWUrA). Is this change a good idea? – HTNW Jun 04 '19 at 14:15
  • @HTNW I don't hink that's a good idea. You are increasing the risk of accidentally creating a dangling reference just for the sake of explicitely doing an optimization that any compiler would do by itself anyways. –  Jun 04 '19 at 14:27
  • It's possible for `operator() &` and `operator() &&` to be different; I don't think the change is an optimization. I can actually use `v_initializer { [] { return std::forward(f)(); } }` (`initializer` as posted by @PasserBy) to emulate the behavior of `fw_initializer { std::forward(f)(); }` (`initializer` as modified by me). I don't think you can accidentally invoke move semantics with either; you need to both use `std::move` and either write the custom lambda or use `std::forward`. – HTNW Jun 04 '19 at 14:42
  • @HTNW `F&& f` is almost certainly a mistake, you now have a dangling reference whenever `initializer` is used outside its full expression. You may forward `f` even without the reference. – Passer By Jun 04 '19 at 17:14
  • OK, so to sum up: if I have `F f` where `F` is immobile and want a `container` built with `f()`, I cannot construct your `initializer` with `f` in it, but I can either capture `f` as a reference in a lambda or make `initializer` hold a reference (`&` or `&&`—there's apparently no difference) itself. I need *a* reference, somewhere, and a dangling reference is inevitable if `f` gets destroyed. However, your `initializer` disallows accidentally using a temporary for `f`, removing one possible source of such references. Thank you for your patience. – HTNW Jun 04 '19 at 18:30