1

In our code, we have many functions that consume numerous const& "configuration" parameters. Since C++ supports only positional arguments, we've decided to pass most of them in the form of a shared config dictionary (map). That is obviously short-sided, because as our code grew, we've lost track of which function consumes which arguments.

One of the ways out of the mess is to group the algorithms into many (potentially overlapping) structures, and pass the structures to the functions instead of the generic map. Some functions will still consume several such structures, though. To further reduce the confusion, we may introduce a convention, where the order of the passed structures is fixed (e.g. alphabetic), although that may feel unnatural for some use cases.

More flexible solution would be to let C++ decide the order itself:

Suppose we have a

struct ConfigParsA {
   string somePath;
   long numberOfIterations;
   /*...*/
};

struct ConfigParsB {
   /*...*/
};

struct ConfigParsC {
   /*...*/
};

struct Result {
   /*...*/
}

Result functionThatUsesLotsOfArguments(long l1, const std::vector<int>& v1, const ConfigParsA& p1, const ConfigParsB& p2, const ConfigParsC& c3);


void main() {
   ConfigPars1 par1{"/home/user/file.csv", 1000, /*...*/};
   ConfigPars2 par2{/*...*/};
   ConfigPars3 par3{/*...*/};
   std::vector<int> vec{1,2,3,4};

   auto result = functionThatUsesLotsOfArguments(42, vec, par2, par3, par1); //Compile error: order of parameters is wrong. 
};

I am thinking, that maybe there is an elegant solution to let the following code to compile:

void main() {
   ConfigPars1 par1{"/home/user/file.csv", 1000, /*...*/};
   ConfigPars2 par2{/*...*/};
   ConfigPars3 par3{/*...*/};
   std::vector<int> vec{1,2,3,4};

   auto curried_f = curry(functionThatUsesLotsOfArguments)(42, vec)
   auto result = callWithAnyOrderOfParameters(curried_f, par2, par3, par1); 
};

I definitely value code simplicity and hate excessive template meta-programming, so I am not really OK with 100+ loc of template code being added to the project just to allow this trick. Maybe it is possible to pull it off relatively easily? Currying is relatively nicely solved (How can currying be done in C++?), so I wonder if there's a way to implement the callWithAnyOrderOfParameters.

Assumptions:

  • functionThatUsesLotsOfArguments has no overloads.
  • Each argument has a distinct, non-convertible type.
Adam Ryczkowski
  • 7,592
  • 13
  • 42
  • 68
  • There's the [Boost parameter library](https://www.boost.org/doc/libs/1_82_0/libs/parameter/doc/html/index.html) which uses some tricks to allow named parameters. Or you could use objects and method chaining (similar to the builder design pattern) to add "arguments" in any order, and the destruction of the object will call the actual function with the arguments in the correct order. – Some programmer dude Jul 12 '23 at 11:22

1 Answers1

2

You can do it with a tuple:

Result functionThatUsesLotsOfArguments_impl(long , const std::vector<int>& , const ConfigParsA& , const ConfigParsB& , const ConfigParsC& )
{
    return {};
}


template<class... Args>
Result functionThatUsesLotsOfArguments(Args&&... arg)
{
    auto arguments = std::tuple<Args...>(std::forward<Args>(arg)...);

    return functionThatUsesLotsOfArguments_impl(
        std::get<long>(arguments),
        std::get<std::vector<int>&>(arguments),
        std::get<ConfigParsA&>     (arguments),
        std::get<ConfigParsB&>     (arguments),
        std::get<ConfigParsC&>     (arguments)
        );
}

demo : https://wandbox.org/permlink/KlATu0uRJkPMqmZJ

Be careful, you have to call:

functionThatUsesLotsOfArguments(long(42), vec, par2, par3, par1);

not

functionThatUsesLotsOfArguments(42, vec, par2, par3, par1);

If not the tuple will not find the longargument.

This is the shortest way I know without library.

Martin Morterol
  • 2,560
  • 1
  • 10
  • 15