Is it possible to in-place initailize std::variant given a factory object and variadic arguments list?
Asked Answered
M

1

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

Mining answered 16/9, 2022 at 11:55 Comment(3)
The T&& constructor has to bind to an object, so no copy elision.Thad
@Thad 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 legitMining
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.Luht
M
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()) { }
};
Marlie answered 16/9, 2022 at 12:49 Comment(7)
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<T> constructor deleted?Mining
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<T>. A delete'd initializer<T> 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?Marlie
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 factoryMining
@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<typename F> void foo(F f) { int i = 5; std::variant<something>(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.Marlie
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?Mining
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).Mining
@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<typename T> variant(T&&) constructor, which is specified to choose the alternative as if by resolving overloads void a(Alt1), a(Alt2), ...;Marlie

© 2022 - 2024 — McMap. All rights reserved.