djsp
djsp

Reputation: 2293

homogenous containers, derived classes, initializer lists and move semantics

I have code that makes use of some dirty tricks to make it appear as what I believe is a nice interface. The most important class, Worker, is designed to make the caller construct it with temporaries; the constructor will then take them. I think the code is self-explanatory:

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

struct Base
{
    virtual void
        print
        ( void )
        {
            cout << "Base" << endl;
        };
};

struct Derived1 : public Base
{
    virtual void
        print
        ( void )
        {
            cout << "Derived1" << endl;
        };
};

struct Derived2 : public Base
{
    virtual void
        print
        ( void )
        {
            cout << "Derived2" << endl;
        };
};

class Worker
{
    private:
        /* Arrays can't hold references, and
         * vectors are homogenous, so the
         * only option is to use (smart) pointers. */
        vector< unique_ptr<Base> >
            V
            ;

        /* The dirty trick I spoke about. */
        template<typename T> void
            init
            ( T && t )
            {
                V.emplace_back( new T( forward<T>(t) ) );
                return;
            };
        template<typename T , typename ... U> void
            init
            ( T && t , U && ... u )
            {
                V.emplace_back( new T( forward<T>(t) ) );
                    /* The usage of std::move() is explained below. */
                init(move(u)...);
                return;
            };

    public:
        template<typename ... T>
            Worker
            ( T && ... t )
            {
                    /* Use std::move() because, inside the body
                     * of the function, the arguments are lvalues.
                     * If I hadn't put std::move(), the compiler
                     * would complain about an attempt of using
                     * _new_ with a reference type (above). */
                init(move(t)...);
                return;
            };

        void
            work
            ( void )
            {
                for ( const auto & x : V )
                    x->print();
                return;
            };
};

int
main
    ( void )
{
    /* The goal: be able to create an instance of Worker
     * passing temporaries to the constructor. No initializer_list
     * is involved, no copies are made; clean, fast moves. */
    Worker worker{ Derived1() , Base() , Derived2() };
    /* This should print "Derived1\nBase\nDerived2\n". */
    worker.work();

    return 0;
}

Even though it compiles fine (g++ 4.8.1) and works out of the box, I feel this is not the best way to achieve my goals, and I'd like to get rid of that annoying feeling. Has anyone found another workaround for this? Are there any better aproaches, any disadvantages with my design?

Thanks in advance. The code should compile just fine, and show how I'd like my interface to be designed and why I've made use of those "tricks". Best regards, Kalrish

Upvotes: 1

Views: 158

Answers (1)

dyp
dyp

Reputation: 39121

Though I still think this question better belongs to Code Review, here's an alternative version for your "dirty trick"

    template < typename T >
    int emplace_back(T&& t)
    {
        V.emplace_back( std::forward<T>(t) );
        return 0;
    }

    template<typename ... T>
        Worker
        ( T && ... t )
        {
            auto i = {emplace_back(new T{forward<T>(t)})...};
        };

Or, if you want to get rid of that member function:

public:
    template<typename ... T>
        Worker
        ( T && ... t )
        {
            using up = std::unique_ptr<Base>;
            auto f = [&](up&& p)
                { V.emplace_back(std::move(p)); return 0; };

            auto i = {f( up{new T{forward<T>(t)}} )...};
        };

You might get a warning because i is unused, though. This variable is only required to build an initializer-list, where a pack-expansion is allowed.


Normally, I'd suggest using an initializer-list instead of the variadic template ctor (that's what they're made for), but they don't support moving out the elements as they don't own their storage.

The same problem occurs if you want to initialize the vector in the mem-initializer-list of the ctor (via pack expansion like in the alternative above).

Upvotes: 1

Related Questions