3

I have a class MyClass that looks like this currently:

class MyClass{
    public:
        MyClass(std::initializer_list<std::initializer_list<float>> arg){
            // some code here
        }
        MyClass(vector<vector<float>> arg){
            // identical code as above block
        }
        // other class members
};

An instance of MyClass can be constructed from both an std::initializer_list<std::initializer_list<float>> type, as well as a vector<vector<float>> type, and the code block to handle the constructor argument is identical. Is there a way to eliminate the code duplication?

Sneftel
  • 40,271
  • 12
  • 71
  • 104
Train Heartnet
  • 785
  • 1
  • 12
  • 24
  • Put the common code in a separate function, that both constructors call? It could be created as a template to accept both argument types. – Some programmer dude Sep 08 '20 at 13:52
  • 3
    Do you want to pass an actual `std::initializer_list>` object to your class or you just want syntax like `MyClass({{1.f, 2.f}, {3.f, 4.f}})`? If the latter, you can just remove initializer_list constructor and rely on `std::vector` constructors. – Yksisarvinen Sep 08 '20 at 13:52
  • 6
    Sounds like a case for a [delegating constructor](https://en.cppreference.com/w/cpp/language/constructor#Delegating_constructor). – 0x5453 Sep 08 '20 at 13:52
  • @0x5453 Oh, a delegated constructor is even better than my function. :) – Some programmer dude Sep 08 '20 at 13:54
  • 2
    Optionally, you could create a single templated constructor handling both cases. If the user of your class passes the wrong type of argument, then there will be build errors. This have the added advantage that it can handle any nested container type using the appropriate interface (like e.g. `std::deque>` or other variants). – Some programmer dude Sep 08 '20 at 13:55
  • 2
    @Yksisarvinen: I wish to have the syntax `MyClass a = {{1.f, 2.f}, {3.f, 4.f}}`, but with just the `std::vector` constructor I'm getting a no matching constructor error. Is it possible to have this syntax with just vectors? – Train Heartnet Sep 08 '20 at 13:59
  • @0x5433 : Thanks, this seems to fit my use case! – Train Heartnet Sep 08 '20 at 14:00
  • @Someprogrammerdude: Ah, I see! I'm new to C++, so will have to read up a bit on templates first. Thanks, I'll keep this in mind! – Train Heartnet Sep 08 '20 at 14:00
  • Hmm, it would need one more set of braces to work with just `std::vector` constructor: `MyClass a = {{{1.f, 2.f}, {3.f, 4.f}}};` ([see it online](https://wandbox.org/permlink/IRYJ5M5db73IZVgG)). I guess delegating constructors are your best choice then. – Yksisarvinen Sep 08 '20 at 14:09
  • `initializer_list` ctors are special in that they allow you to pass multiple arguments even if your ctor only has one parameter. `vector v{1,2,3}` - the ctor only has one parameter (`initializer_list`). Similarly, your class: If you want to allow `MyClass m{a0, a1, a2};` then you need either an `initializer_list` ctor or a variadic template ctor (or aggregate init). The template won't work because you can't deduce any type from `{..}`. I would forward both ctors to a (private) ctor template that can deal with any sequence of sequences. – dyp Sep 08 '20 at 16:31

2 Answers2

3

Is there a way to eliminate the code duplication?

Yes. The easy way is to provide only constructor taking the vector as argument (i.e. MyClass(std::vector<std::vector<float>> arg)) and give an extra set of parenthesis to pass the vector list.

class MyClass 
{
   std::vector<std::vector<float>> mStorage;

public:
   MyClass(std::vector<std::vector<float>> arg)
      : mStorage{ std::move(arg) } // use member initializer lists here!
   {}
};

Now you can

MyClass obj1{ { {1.f, 2.f}, {3.f, 4.f} } }; // note the extra set of {}!
// and
auto vec2D = std::vector<std::vector<float>>{ {1.f, 2.f}, {3.f, 4.f} };
MyClass obj2{ std::move(vec2D) };

See a Demo


However, if you insist to keep them both and have the common code, I would suggest a template member function which does this for you.

class MyClass 
{
   std::vector<std::vector<float>> mStorage;

   template<typename Type>
   void setStorage(Type list) /* noexcept */
   {
      mStorage.assign(std::cbegin(list), std::cend(list));
   }

public:
   MyClass(const std::initializer_list<std::initializer_list<float>> arg) {
      this->setStorage(arg);
   }
   MyClass(std::vector<std::vector<float>> arg) {
      this->setStorage(std::move(arg));
   }
};

Now you can write

MyClass obj1{ {1.f, 2.f}, {3.f, 4.f} };  // uses std::initializer_list cont'r
// and
auto vec2D = std::vector<std::vector<float>>{ {1.f, 2.f}, {3.f, 4.f} };
MyClass obj2{ std::move(vec2D) }; // uses std::vector cont'r

See a Demo

JeJo
  • 30,635
  • 6
  • 49
  • 88
2

My solution:

#include <iostream>
#include <vector>

class MyClass
{
  public:
    using Vec_vec = std::vector<std::vector<float>>;
    MyClass(Vec_vec arg) : vv{std::move(arg)}
    {
        for (const auto& ilist : vv)
        {
            for (const auto& x : ilist)
            {
                s += x;
            }
        }
    }

    MyClass(std::initializer_list<std::vector<float>> arg) : MyClass(Vec_vec(arg.begin(), arg.end())) {}

    Vec_vec vv;
    float s {0};
};

int main()
{
    std::vector<std::vector<float>> v = {{1, 2, 3}, {1, 2, 3}, {4}};
    MyClass a{{1, 2, 3}, {2, 3, 4}, {5}};
    MyClass b{v};
    std::cout << a.s << " " << a.vv[0][0] << "\n";  // 1 + 2 + 3 + 2 + 3 + 4 + 5 = 20
    std::cout << b.s << " " << b.vv[0][0] << "\n";  // 2*(1 + 2 + 3) + 4 = 16
}

Remarks, in order of occurrence in the code.

  1. using declaration is just to shorten the notation (and parametrize the code). Irrelevant to the problem.
  2. In the second constructor I use a list of vectors rather than a list of lists as you do. Not only does it allow the same usage patterns as you require, but it should facilitate optimizations, e.g. via std::move as well.
  3. This is really important: I use a delegating constructor Delegate Constructor C++ and https://en.cppreference.com/w/cpp/language/constructor (the second constructor in my example). Here I use an std::vector's constructor that constructs a vector from a sequence defined by two iterators. This solution really avoids duplicating the code
  4. I really don't know if the vectors are copied or moved here. If performance is an issue for you and if they are copied rather then moved (quite likely in my opinion), a special function that changes a list of vectors into a vector of vectors, and which moves the vectors, should be written and used in the delegating constructor to please the compiler.
  5. I assume you know better than I if you need to pass args by value, const reference etc., so this is out of the scope of the question, but certainly worth thinking of. For this reason readers of this answer should not take everything in the example as "certain".
zkoza
  • 2,644
  • 3
  • 16
  • 24