Is there a way to make a constructor accept two possible types of parameters?

Viewed 164

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?

2 Answers

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

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".
Related