Search code examples
c++c++17idiomsnoncopyable

How to write a ctor for a class owning nocopy-nomove types of which one is to be init.ed from another one that can be one out of several types?


I start from a scenario like this:

  • the non-copiable-nor-movable classes Foo1, Foo2, and Baz are untouchable
    struct Foo1 { Foo1(int); Foo1(Foo1&&) = delete; }; // I can't touch it
    struct Foo2 { Foo2(int); Foo2(Foo2&&) = delete; }; // I can't touch it
    struct Baz { Baz(Foo1&); Baz(Foo2&); Baz(Baz&&) = delete; }; // I can't touch it
    
  • a fourth class, Bar, I have control of, looks initially like this:
    struct Bar {
        Bar(int i)
            : i{i}
            , foo{i}
            , baz{foo}
        {}
        int i;
        Foo1 foo;
        Baz baz;
    };
    

At a later point, the need arises that Bar::foo be actually either a Foo1 or a Foo2 depending on the i fed to Bar's constructor.

My initial thought was to use a std::variant, but changing Bar to fulfil the new requirement is not entirely straightforward, because the non-copiable-nor-movable-ity of Foo1, Foo2, and Baz, gets in the way:

  • changing Foo1 foo; to std::variant<Foo1, Foo2> foo; causes in the initialization foo{i} to fail to compile, and that's normal, but let's also simplify things by saying that for now we're still only putting a Foo1 in foo, by changing also foo{i} to foo{Foo1{i}}; this still doesn't work because of the non-movability of Foo1;
  • so I think, ok, I need to construct the Foo1 in place in the std::variant; this would be viable, if Bar did not have a Baz member, because I could make the std::variant default constructible by including std::monostate in it (and this would also allow the conditional construction of Foo1 or Foo2:
    struct Bar {
        Bar(int i)
            : i{i}
            //, baz{foo}
        {
            if (i == 0) {
                foo.emplace<Foo1>(i);
            } else {
                foo.emplace<Foo2>(i);
            }
        }
        int i;
        std::variant<std::monostate, Foo1, Foo2> foo{};
        //Baz baz;
    };
    

The above observations lead me to think that a solution could be using std::unique_ptr, but it does too require quite a bit of code, because one must std::visit in order to construct Baz:

struct Bar {
    using Var = std::variant<std::unique_ptr<Foo1>, std::unique_ptr<Foo2>>;
    Bar(int i)
        : i{i}
        , foo{i == 0 ? Var{std::make_unique<Foo1>(i)} : Var{std::make_unique<Foo2>(i)}}
        , baz{std::visit([](auto&& p) { return Baz{*p}; }, foo)}
    {}
    int i;
    Var foo;
    Baz baz;
};

I was wondering if I have missed some cleaner way to deal with such a scenario.


Solution

  • You can construct the variant in place, no need of std::monostate:

    struct Bar {
        using Foo = std::variant<Foo1, Foo2>;
        Bar(int i)
            : i{i}
            , foo{i == 0 ? Foo(std::in_place_type<Foo1>, i) : Foo(std::in_place_type<Foo2>, i)}
            , baz{std::visit([](auto&& p) { return Baz{p}; }, foo)}
        {}
        int i;
        std::variant<Foo1, Foo2> foo;
        Baz baz;
    };
    

    Demo

    Unfortunately, msvc rejects that code, but using intermediate function (as lambda) works :

    struct Bar {
        using Foo = std::variant<Foo1, Foo2>;
        Bar(int i)
            : i{i}
            , foo{[](int i){
                if (i == 0) {
                    return Foo(std::in_place_type<Foo1>, i);
                } else {
                    return Foo(std::in_place_type<Foo2>, i);
                }
            }(i)}
            , baz{std::visit([](auto&& p) { return Baz{p}; }, foo)}
        {}
        int i;
        std::variant<Foo1, Foo2> foo;
        Baz baz;
    };
    

    Demo