Make argument mandatory without relying on position

87 views Asked by At

Say a class Foo has two dependencies (Bar and Baz), and that it is an error to construct a Foo without providing both of them. Constructor injection makes it easy to guarantee at compile time that this is done:

class Foo
{
public:
   Foo(const std::shared_ptr<Bar>& bar, const std::shared_ptr<Baz>& baz);
   // (don't get hung up on the type of pointer used; it's for example only)
};

But let's say Foo also needs two doubles:

class Foo
{
public:
    Foo(const std::shared_ptr<Bar>& bar, const std::shared_ptr<Baz>& baz,
        double val1, double val2);
};

Now there is a problem; it would be really easy for the caller to accidentally transpose val1 and val2 and create a runtime bug. We can add a Params struct to allow named initialization and preclude this:

class Foo
{
public:
   struct Params
   {
       std::shared_ptr<Bar> bar;
       std::shared_ptr<Baz> baz;
       double val1;
       double val2
   };

    Foo(const Params& params);
};

// ...

std::shared_ptr<Foo> MakeDefaultFoo()
{
    Foo::Params p;
    p.bar = std::make_shared<Bar>();
    p.baz = std::make_shared<Baz>();
    p.val1 = 4.0;
    p.val2 = 3.0;
    return std::make_shared<Foo>(p);
}

But now we have the problem that the caller might forget to populate one of the fields in Params, which would not be detectable until runtime. struct initialization syntax or an initializer list would make it impossible to forget a field, but then we're back to relying on position!

Is there some trick that makes it possible to have the best of both worlds--compiler-enforced mandatory arguments that are assigned by name instead of position?

3

There are 3 answers

6
Slava On BEST ANSWER

Just have a simple wrapper may work:

template <typename Tag, typename T>
struct Argument {
    explicit Argument( const T &val );
    T get() const;
};

class Foo {
public:
       struct Val1Tag;
       struct Val2Tag;
       typedef Argument<Val1Tag,double> Val1;
       typedef Argument<Val2Tag,double> Val2;

       Foo( Val1 v1, Val2 v2 );

};

Foo foo( Foo::Val1( 1.0 ), Foo::Val2( 2.3 ) );

Now types are explicit and you cannot swap them without getting compiler error.

1
Barry On

Very curious to see what cdhowie is tinkering with, but in the meantime, a simple wrapper with different types might solve some problems:

struct Val1 {
    explicit Val1(double v) : v(v) { }
    operator double() const { return v; }

    double v;
};

// copy for Val2

class Foo
{
public:
    Foo(const std::shared_ptr<Bar>& bar, const std::shared_ptr<Baz>& baz,
        Val1 val1, Val2 val2);
};

This way you can't mix them up, since you'll have to construct a Foo like:

Foo foo(bar, baz, Val1{3.0}, Val2{7.0});

It's a bunch of extra typing to make sure the types are different, and you definitely have to make sure you make the constructor explicit (or it defeats the point), but it helps.

0
n. m. could be an AI On

Something like this (untested)

template <typename tag, typename t>
struct param
{
   explicit param(t vv)
    : v(vv) {}
   param(const param& p)
    : v(p.v) {}
   t v; 
};

struct one{}; struct two {};
using paramone = param<one, double>;
using paramtwo = param<two, double>;

void somefunc (paramone p1, paramtwo p2)
{ ... };
void somefunc (paramtwo p2, paramone p1) 
{ somefunc(p1, p2); }

// using it

somefunc (2, 3); // bad
somefunc (paramone(2), paramtwo(3)); // good
somefunc (paramtwo(3), paramone(2)); // also good