1

std::variant has a constructor that accepts std::in_place_t<Type>, Args &&... arguments that results in in-place construction.

    {
        std::cout << "in_place_type:\n";
        const auto var = std::variant<callable>(std::in_place_type<callable>);
        std::cout << "finished\n";
    }

I wonder if it is somehow possible to in-place construct given a Factory object:

    {
        std::cout << "by factory:\n";
        const auto var = std::variant<callable>{[] ()-> callable {return {}; }()};
        std::cout << "finished\n";
    }

https://godbolt.org/z/rY5WG1hGn

#include <variant>

#include <iostream>

int main(int, char **)
{
    struct callable
    {
        callable()
        {
            std::cout << "callable()" << std::endl;
        }

        ~callable()
        {
            std::cout << "~callable()" << std::endl;
        }
    };

    {
        std::cout << "in_place_type:\n";
        const auto var = std::variant<callable>(std::in_place_type<callable>);
        std::cout << "finished\n";
    }
    std::cout << '\n';
        
    {
        std::cout << "by factory:\n";
        const auto var = std::variant<callable>{[] ()-> callable {return {}; }()};
        std::cout << "finished\n";
    }
}

I think it does not work in this case because std::variant(T &&t) constructor is used with the factory, and it can not use copy elision.


I tried to elaborate on HTNW's answer (create an onbject from arguments), but get a compiler error:

#include <variant>
#include <tuple>

struct to_construct {
  // must NOT exist
  // template<typename F>
  // to_construct(initializer<F>) { std::cout << "Foiled!\n"; }
  // or (more likely)
  // template<typename T>
  // to_construct(T) { std::cout << "Foiled again!\n"; }
  to_construct() = default;
  to_construct(to_construct&&) = delete;
  to_construct(to_construct const&) = delete;
};

#include <iostream>

struct callable
{
    callable(int i)
    {
        std::cout << "callable():" << i << std::endl;
    }

    ~callable()
    {
        std::cout << "~callable()" << std::endl;
    }
};

template<typename T>
struct box {
  T x;
  template<typename F>
  box(F f) 
  : x(f())
  {}
};

template<typename F>
struct initializer {
  F init;
  operator auto() {
    return init();
  }
};

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

template <typename ... Types, typename Factory, typename ... Args>
std::variant<box<Types>...> variant_from(Factory &&f, Args &&... args)
{
    return {
        initializer{
            [tupleArgs = std::forward_as_tuple(args...),
            f = std::forward<Factory>(f)](){
                return std::apply(f, tupleArgs);
            }
        }
    };
}


int main() 
{  
  {
      const auto var = 
        std::variant<to_construct>(initializer{
            []() -> to_construct { 
                std::cout << "Success\n";
                return {}; 
                }
            });
  }
   
  {
      const auto var = variant_from<callable>([](int i) { return callable(i);}, 42);
  }
}
<source>: In instantiation of 'box<T>::box(F) [with F = initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >; T = callable]':
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:283:4:   required from 'constexpr std::__detail::__variant::_Uninitialized<_Type, false>::_Uninitialized(std::in_place_index_t<0>, _Args&& ...) [with _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Type = box<callable>]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:385:4:   required from 'constexpr std::__detail::__variant::_Variadic_union<_First, _Rest ...>::_Variadic_union(std::in_place_index_t<0>, _Args&& ...) [with _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _First = box<callable>; _Rest = {}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:460:4:   required from 'constexpr std::__detail::__variant::_Variant_storage<false, _Types ...>::_Variant_storage(std::in_place_index_t<_Np>, _Args&& ...) [with long unsigned int _Np = 0; _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Types = {box<callable>}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:557:20:   required from 'constexpr std::__detail::__variant::_Variant_base<_Types>::_Variant_base(std::in_place_index_t<_Np>, _Args&& ...) [with long unsigned int _Np = 0; _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Types = {box<callable>}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:1448:57:   required from 'constexpr std::variant<_Types>::variant(std::in_place_index_t<_Np>, _Args&& ...) [with long unsigned int _Np = 0; _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Tp = box<callable>; <template-parameter-2-4> = void; _Types = {box<callable>}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:1419:27:   required from 'constexpr std::variant<_Types>::variant(_Tp&&) [with _Tp = initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >; <template-parameter-2-2> = void; <template-parameter-2-3> = void; _Tj = box<callable>; <template-parameter-2-5> = void; _Types = {box<callable>}]'
<source>:61:5:   required from 'std::variant<box<Types>...> variant_from(Factory&&, Args&& ...) [with Types = {callable}; Factory = main()::<lambda(int)>; Args = {int}]'
<source>:78:46:   required from here
<source>:36:8: error: no match for call to '(initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >) ()'
   36 |   : x(f())
      |       ~^~
ASM generation compiler returned: 1
<source>: In instantiation of 'box<T>::box(F) [with F = initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >; T = callable]':
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:283:4:   required from 'constexpr std::__detail::__variant::_Uninitialized<_Type, false>::_Uninitialized(std::in_place_index_t<0>, _Args&& ...) [with _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Type = box<callable>]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:385:4:   required from 'constexpr std::__detail::__variant::_Variadic_union<_First, _Rest ...>::_Variadic_union(std::in_place_index_t<0>, _Args&& ...) [with _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _First = box<callable>; _Rest = {}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:460:4:   required from 'constexpr std::__detail::__variant::_Variant_storage<false, _Types ...>::_Variant_storage(std::in_place_index_t<_Np>, _Args&& ...) [with long unsigned int _Np = 0; _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Types = {box<callable>}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:557:20:   required from 'constexpr std::__detail::__variant::_Variant_base<_Types>::_Variant_base(std::in_place_index_t<_Np>, _Args&& ...) [with long unsigned int _Np = 0; _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Types = {box<callable>}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:1448:57:   required from 'constexpr std::variant<_Types>::variant(std::in_place_index_t<_Np>, _Args&& ...) [with long unsigned int _Np = 0; _Args = {initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >}; _Tp = box<callable>; <template-parameter-2-4> = void; _Types = {box<callable>}]'
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/variant:1419:27:   required from 'constexpr std::variant<_Types>::variant(_Tp&&) [with _Tp = initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >; <template-parameter-2-2> = void; <template-parameter-2-3> = void; _Tj = box<callable>; <template-parameter-2-5> = void; _Types = {box<callable>}]'
<source>:61:5:   required from 'std::variant<box<Types>...> variant_from(Factory&&, Args&& ...) [with Types = {callable}; Factory = main()::<lambda(int)>; Args = {int}]'
<source>:78:46:   required from here
<source>:36:8: error: no match for call to '(initializer<variant_from<callable, main()::<lambda(int)>, int>(main()::<lambda(int)>&&, int&&)::<lambda()> >) ()'
   36 |   : x(f())
      |       ~^~
Execution build compiler returned: 1

https://godbolt.org/z/8MMMEe3jx

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • The `T&&` constructor has to bind to an object, so no copy elision. – NathanOliver Sep 16 '22 at 12:02
  • @NathanOliver I guess. There is no constructor overload for `std::variant` that would take a factory object. I am just not sure if there may be any workarounds. So the answer saying "it is not possible" would be legit – Sergey Kolesnik Sep 16 '22 at 12:09
  • If you are using `box` you are not supposed to use `initializer` as well as explained in the answer. There is no need to use the init-captures in `variant_from`. Just `[&]` will be fine. – user17732522 Sep 16 '22 at 15:07

1 Answers1

3

Yes.

The factory needs to be wrapped in a type with a conversion to the contained type, and the contained type needs to not have a constructor that would take precedence.

template<typename F>
struct initializer {
  F init;
  operator auto() {
    return init();
  }
};
template<typename F>
initializer(F) -> initializer<F>;

struct to_construct {
  // must NOT exist
  // template<typename F>
  // to_construct(initializer<F>) { std::cout << "Foiled!\n"; }
  // or (more likely)
  // template<typename T>
  // to_construct(T) { std::cout << "Foiled again!\n"; }
  to_construct() = default;
  to_construct(to_construct&&) = delete;
  to_construct(to_construct const&) = delete;
};

int main() {
  std::variant<to_construct, int> v;
  v.emplace<to_construct>(initializer{[]() -> to_construct { std::cout << "Success\n"; return {}; }});
}

The point that the contained type cannot have a constructor matching initializer is important: it means you cannot safely do this trick for variants of types that you do not control. If you need to construct some external T in a variant in this way, instead wrap the T in a box of your own control, in which case you can just add a constructor from a factory directly anyway.

// do NOT do this, because it might fail silently and weirdly
// template<typename T>
// void foo() {
//   std::variant<int, T> v;
//   v.emplace<1>(initializer{[]() -> T { return {}; }});
// }

template<typename T>
struct box {
  T x;
  template<typename F>
  box(F f) : x(f()) { }
};
template<typename T>
void foo() {
  std::variant<int, box<T>> v;
  v.template emplace<1>([]() -> T { return {}; });
}

I.e. you can construct an object in a variant from a factory without changing the variant type sometimes (when you control the alternatives), but in general you may need to change some alternatives into boxs.


The issue with your expanded code based on the above is that you try to use initializer with box. box does not need initializer. It takes lambdas directly. Further, since the initializing lambda does not have to live past the construction of the variant, it should pretty much never capture by value. Also, if you don't want to specify which alternative you want manually every time, you should make box's constructor drop out of overload resolution if it would not work.

// it is convention in the C++ standard libraries to not bother forwarding functors
template<typename... Types, typename Factory, typename... Args>
std::variant<box<Types>...> variant_from(Factory f, Args&&... args) {
    return [&]() { return f(std::forward<Args>(args)...); };
}

template<typename T>
struct box {
  T x;
  template<
    typename F,
    typename = std::enable_if_t< // unsure if this is quite the right condition, it's a bit stricter than just "x(f()) would compile"
      std::is_same_v<
        T,
        std::decay_t<decltype(std::declval<F&>()())>>>>
  box(F f) : x(f()) { }
};
HTNW
  • 27,182
  • 1
  • 32
  • 60
  • So what if I have *no* control on whether the variant's types have or don't have move constructors? Would it help to wrap them inside a non-movable and non-copyable objects within a variant? And what about factories that construct from arguments? Would it suffice to mark a `initializer` constructor `delete`d? – Sergey Kolesnik Sep 16 '22 at 13:43
  • If someone else is setting the `variant` type *and* you can't ensure the alternative you're constructing lacks constructors matching `initializer`, you're out of luck. Nothing in my code requires move constructors. I don't know what wrapping you're suggesting; the only one you should need is `box`. A `delete`'d `initializer` constructor would also break things. It needs to *not be declared*. It doesn't make sense to have a factory with arguments here. Do you mean a factory that e.g. captures variables and returns objects made from those? – HTNW Sep 16 '22 at 13:51
  • https://godbolt.org/z/4d6Px41r4 the second one does not compile. Also, in my case, the user provides a `Factory(args...)`, and I am initializing the variant by providing the actual arguments for the factory – Sergey Kolesnik Sep 16 '22 at 13:54
  • @SergeyKolesnik `box` is meant to be constructed directly from a lambda. It doesn't need `initializer`. Remove the `initializer` and it compiles. And again, it does not make sense for the initializer to have arguments. You mean you're doing something like `template void foo(F f) { int i = 5; std::variant(initializer{[&]() -> something { return f(i, "a", false); }});`. The initializer does not have arguments (empty `()` in lambda), can not have arguments, and does not need arguments. – HTNW Sep 16 '22 at 14:28
  • *it is convention in the C++ standard libraries to not bother forwarding functors* - what if one's factory is stateful, and is impossible to copy? And is owned by the caller? – Sergey Kolesnik Sep 17 '22 at 09:44
  • could you also mention in your answer, how can a variant deduce the `box`'s specialization? There are no deduction guides in the code, and `box`'s constructor is a template accepting `F` callable. I see that `T` will be deduced only within a template constructor form `F::operator()`'s return value. Also there is no overload for variant's constructor that accepts "Args...", which I think lambda *is* in this case (construct `box` in-place from callable). – Sergey Kolesnik Sep 17 '22 at 10:11
  • @SergeyKolesnik For a noncopyable factory, you simply pass `std::ref(f)` instead of `f`. That's why the standard library doesn't bother avoiding copies of functors. You can make `box`'s constructor use SFINAE for the latter issue. We are using the `template variant(T&&)` constructor, which is specified to choose the alternative as if by resolving overloads `void a(Alt1), a(Alt2), ...;` – HTNW Sep 17 '22 at 17:04