4

I have a class that looks like the following:

class Example { Foo1 f1; Foo2 f2; Foo3 f3; size_t hash; }; 

Please note that FooX will be always different classes. I want to add a constructor which assigns the value to each fX element and computes the hash (for cacheing its value).

However I want to be able to omit any number of arguments and assign a default value to the corresponding member of the class. Obviously I could use the defaulting feature of C++: Example(Foo1 f1 = DEFAULT4F1, Foo2 f2 = DEFAULT4F2, Foo3 f3 = DEFAULT4F3); However for being able to omit f2, I need to omit f3, which I might want to specify. Then I could write a constructor for each possible order, but the amount of constructors grows with the factorial of elements, so for this case:

Example(Foo1 f1 = DEFAULT4F1, Foo2 f2 = DEFAULT4F2, Foo3 f3 = DEFAULT4F3); Example(Foo1 f1 = DEFAULT4F1, Foo3 f3 = DEFAULT4F3, Foo2 f2 = DEFAULT4F2); Example(Foo2 f2 = DEFAULT4F2, Foo1 f1 = DEFAULT4F1, Foo3 f3 = DEFAULT4F3); Example(Foo2 f2 = DEFAULT4F2, Foo3 f3 = DEFAULT4F3, Foo1 f1 = DEFAULT4F1); Example(Foo3 f3 = DEFAULT4F3, Foo1 f1 = DEFAULT4F1, Foo2 f2 = DEFAULT4F2); Example(Foo3 f3 = DEFAULT4F3, Foo2 f2 = DEFAULT4F2, Foo1 f1 = DEFAULT4F1); 

I don't want to specify the same constructor that many cases, because in my real case there are more than 3 parameters. I also want to consider forwarding their values (Foo&& arg and std::forward(arg))

Edit: To sum up: desired constructor would act like this, but being able to omit any of the parameters, and use a default value for them

Example(Foo1&& f1, Foo2&& f2, Foo3&& f3) : f1(std::forward(f1)), f2(std::forward(f2)), f3(std::forward(f3)), hash(doSomethingForIt()) { } 
5
  • Use a parameter object Commented Aug 20, 2019 at 19:12
  • 4
    Builder pattern might come to help. Commented Aug 20, 2019 at 19:13
  • XY problem? Bad design? Commented Aug 20, 2019 at 19:14
  • 3
    Maybe use designated initializers? Commented Aug 20, 2019 at 19:26
  • It still grows significantly with each new member but you could cut the number of required variations significantly if you require the initialization values (if present) be in a particular order. That is, my_class m{f1, f2, f3}; and my_class m2{f1, f3}; are both legal but my_class m3{f1, f3, f2}; is not. Commented Aug 20, 2019 at 19:27

4 Answers 4

3

This is a workaround, not an initialization in C++ sense.

Because you need in advance all members of your class, you already know them:

class Example { Foo1 f1; Foo2 f2; Foo3 f3; size_t hash; //default ctor, initializes all members with defaults defined somewhere Example() : f1(defaultF1), f2(defaultF2), f3(defaultF3) {} .... 

Now we add setters:

 Example& setFoo1(Foo1 fo1 = defaultF1) { f1 = fo1; return *this; } Example& setFoo2(Foo2 fo2 = defaultF2) { f2 = fo2; return *this; } .... }; //class Example 

And then the usage:

Example exple; exple.setFoo2(someF2value).setFoo1(someF1value); //not all setFooX are used 

Notice that we can call setFooX() in any order.

Sign up to request clarification or add additional context in comments.

Comments

3

Adding to Ripi2 solution I would also add a method which computes the hash and returns an r-value.

class Example { Foo1 f1 = DEFAULT4F1; Foo2 f2 = DEFAULT4F2; Foo3 f3 = DEFAULT4F3; size_t hash; auto doSomethingForIt(); public: Example() = default; auto& initFoo1(Foo1&& f) { f1 = std::move(f); return *this; } auto& initFoo2(Foo2&& f) { f2 = std::move(f); return *this; } auto& initFoo3(Foo3&& f) { f3 = std::move(f); return *this; } auto&& finalize() { hash = doSomethingForIt(); return std::move(*this); } }; 

This allows for stuff like this to work.

void foo(Example&& e); foo(Example().initFoo1(f1).initFoo3(f3).initFoo2(f2).finalize()); 

Comments

1

I propose a solution where the constructor takes a parameter pack of arguments, forms a tuple with it and tries to reorder the tuple to the desired configuration.

#include <tuple> namespace details { // General case // In : the type of input tuple with the out-of-order elements // Out : a tuple with the desired element order template<class In, class Out> struct sort_tuple_impl; // Implementation with specialization template<class In, class ... T> struct sort_tuple_impl<In, std::tuple<T...>> { static auto sort(In && params) { // Construct another tuple using the output type's elements // to determine the order of the elements return std::make_tuple(std::move(std::get<T>(params))...); } }; } // Reorders a tuple template<class Out, class In> Out sort_tuple(In && params) { return details::sort_tuple_impl<In, Out>::sort(std::move(params)); } // Example types struct Foo1 {}; struct Foo2 {}; struct Foo3 {}; class Example { public: // A constructor that takes any kind and number of argument template<class ... T> explicit Example(T&& ... params) : // Make a tuple with the arguments and reorder it according to `decltype(f)` f{sort_tuple<decltype(f)>(std::make_tuple(std::forward<T>(params)...))} { } private: // The elements you want to be able to initialize std::tuple<Foo1, Foo2, Foo3> f; }; 

Usage example :

int main() { Foo1 f1; Foo2 f2; Foo3 f3; Example e1{ f1, f2, f3 }; Example e2{ f2, f1, f3 }; Example e3{ f3, f2, f1 }; Example e4{ f3, f2 ,f1 }; // These fail to compile // Example e5{ f1 }; // Example e6{ f1, f1, f2}; // Example e7{ f2, f3, f1, f3 }; } 

Note that if the user provides the same Foo more than once or fails to provide one this will produce a compiler error because std::get fails if the requested type is not present exactly once. From std::get :

Extracts the element of the tuple t whose type is T. Fails to compile unless the tuple has exactly one element of that type.

It doesn't produce great error messages, but it prevents mistakes.

Edit : This fails the apparent requirement that some elements can be omitted. But I'll leave it here nonetheless as it may be useful. Perhaps another templated function could be used to add the default values to the missing elements.

Comments

1

I gave a second try to François Andrieux's solution and I achieved my requirements. Here is a working example (C++17) of it:

#include <tuple> #include <iostream> //Make a unique class for each foo, to prevent implicit casting template <typename P> struct Foo{ constexpr Foo(int x) : x(x) {} int x; }; using Foo1=Foo<struct Phatom_1>; using Foo2=Foo<struct Phatom_2>; using Foo3=Foo<struct Phatom_3>; //Example class class Example { public: //Default constructor, assingns default falues to all members Example() : f1(DEFAULT_F1), f2(DEFAULT_F2), f3(DEFAULT_F3) { } template<typename... Types> Example(Types&&... t) : Example() { //First of all, calls the default constructor auto paramTpl = std::make_tuple(std::forward<Types>(t)...); //Creates a tuple with the parameters assign(f1, paramTpl); assign(f2, paramTpl); assign(f3, paramTpl); } void show() const{ std::cout << "f1=" << f1.x << ", f2=" << f2.x << ", f3=" << f3.x << std::endl; } private: static constexpr auto DEFAULT_F1 = Foo1(1); static constexpr auto DEFAULT_F2 = Foo2(32); static constexpr auto DEFAULT_F3 = Foo3(-1); template<typename Type, typename... Types> static void assign(Type& el, std::tuple<Types...> tpl){ struct has_type : std::disjunction<std::is_same<Type, Types>...>{}; if constexpr (has_type::value){ el = std::move(std::get<Type>(tpl)); } } Foo1 f1; Foo2 f2; Foo3 f3; }; int main(){ Example ex1; ex1.show(); Example ex2(Foo1(1), Foo3(1), Foo2(1)); ex2.show(); Example ex3(Foo1(2), Foo3(2)); ex3.show(); return 0; } 

Edit: I have simplified a bit

1 Comment

Glad my answer was useful.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.