Pregunta

I've recently run into a quite a few situations where the Named Parameter Idiom would be useful, but I'd like it to be guaranteed in compile-time. The standard method of returning references in a chain almost always appears to invoke a run-time constructor (compiling with Clang 3.3 -O3).

I haven't been able to find anything with reference to this so I tried to get this to work with constexpr and got something functional:

class Foo
{
private:
    int _a;
    int _b;
public:
    constexpr Foo()
        : _a(0), _b(0)
    {}
    constexpr Foo(int a, int b)
        : _a(a), _b(b)
    {}
    constexpr Foo(const Foo & other)
        : _a(other._a), _b(other._b)
    {}
    constexpr Foo SetA(const int a) { return Foo(a, _b); }
    constexpr Foo SetB(const int b) { return Foo(_a, b); }
};
...
Foo someInstance = Foo().SetB(5).SetA(2); //works

While this is okay for a small number of parameters, for larger numbers it quickly blows up into a mess:

    //Unlike Foo, Bar takes 4 parameters...
    constexpr Bar SetA(const int a) { return Bar(a, _b, _c, _d); }
    constexpr Bar SetB(const int b) { return Bar(_a, b, _c, _d); }
    constexpr Bar SetC(const int c) { return Bar(_a, _b, c, _d); }
    constexpr Bar SetD(const int d) { return Bar(_a, _b, _c, d); }

Is there a better way? I'm looking at doing this with classes that have many (30+) parameters and this seems like it would be prone to error if extended in the future.

EDIT: Removed C++1y tag -- while C++1y does appear to fix the problem (thanks TemplateRex!) this is for production code, and we are stuck with C++11. If that means its impossible, then I guess that's just the way it is.

EDIT2: To show why I'm looking for this, here's a use case. Currently with our platform, developers need to explicitly set bit vectors for hardware configurations, and while this is okay it's very error prone. Some are using designated initializers from the C99 extension, which is okay but non-standard:

HardwareConfiguration hardwareConfig = {
    .portA = HardwareConfiguration::Default,
    .portB = 0x55,
    ...
};

Most, however, aren't even using this, and are just inputting a blob of numbers. So as a working improvement, I'd like to move towards something like this (since it also forces better code):

HardwareConfiguration hardwareConfig = HardwareConfiguration()
    .SetPortA( Port().SetPolarity(Polarity::ActiveHigh) )
    .SetPortB( Port().SetPolarity(Polarity::ActiveLow) );

Which might be far more verbose, but much clearer when reading later.

¿Fue útil?

Solución

Using Template Metaprogramming

Here is something I came up with to solve your problem (at least partially). By using template metaprogramming, you can leverage the compiler to do most of the job for you. These techniques look weird for those who have never seen such code before, but thankfully most of the complexity can be hidden away in a header and the users only interact with the library in a neat and terse manner.

A Sample Class Definition and its Use

Here is an example of what defining a class would entail on your part:

template <
    //Declare your fields here, with types and default values
    typename PortNumber = field<int, 100>, 
    typename PortLetter = field<char, 'A'>
>
struct MyStruct : public const_obj<MyStruct, PortNumber, PortLetter>  //Derive from const_obj like this, passing the name of your class + all field names as parameters
{
    //Your setters have to be declared like this, by calling the Set<> template provided by the base class
    //The compiler needs to be told that Set is part of MyStruct, probably because const_obj has not been instantiated at this point
    //in the parsing so it doesn't know what members it has. The result is that you have to use the weird 'typename MyStruct::template Set<>' syntax
    //You need to provide the 0-based index of the field that holds the corresponding value
    template<int portNumber>
    using SetPortNumber = typename MyStruct::template Set<0, portNumber>;

    template<int portLetter>
    using SetPortLetter = typename MyStruct::template Set<1, portLetter>;

    template<int portNumber, char portLetter>
    using SetPort = typename MyStruct::template Set<0, portNumber>
                           ::MyStruct::template Set<1, portLetter>;


    //You getters, if you want them, can be declared like this
    constexpr int GetPortNumber() const
    {
        return MyStruct::template Get<0>();
    }

    constexpr char GetPortLetter() const
    {
        return MyStruct::template Get<1>();
    }
};

Using the Class

int main()
{
    //Compile-time generation of the type
    constexpr auto myObject = 
        MyStruct<>
        ::SetPortNumber<150>
        ::SetPortLetter<'Z'>();

    cout << myObject.GetPortNumber() << endl;
    cout << myObject.GetPortLetter() << endl;
}

Most of the job is done by the const_obj template. It provides a mechanism to modify your object at compile time. Much like a Tuple, the fields are accessed with 0-based indices but this does not stop you from wrapping the setters with friendly names, as is done with SetPortNumber and SetPortLetter above. (They just forward to Set<0> and Set<1>)

About Storage

In the current implementation, after all the setters have been called and the object declared, the fields end up being stored in a compact array of const unsigned char's named data in the base class. If you use fields that are not unsigned chars (as in done above with PortNumber for example) the field is divided in big endien unsigned char's (could be changed to little endien as needed). If you don't need an actual storage that has an actual memory address, you could omit it altogether by modifying the packed_storage (see full implementation link below) and the values would still be accessible at compile time.

Limitations

This implementation only allows integral types to be used as fields (all flavors of shorts, ints, longs, bool, char). You can still provide setters that act on more than one field though. Example:

template<int portNumber, char portLetter>
using SetPort = typename MyStruct::template Set<0, portNumber>::
                         MyStruct::template Set<1, portLetter>;

Full Code

The full code for the implementation of this little library can be found here:

Full Implementation

Additional Notes

This code has been tested and works with the C++11 implementation of both g++ and clang. It has not been tested for hours and hours so of course there may be bugs but it should provide you with a good base to start with. I hope this helps!

Otros consejos

In C++14, constraints on constexpr function will be relaxed, and the usual chaining of reference-returning setters will work at compile-time:

#include <iostream>
#include <iterator>
#include <array>
#include <utility>

class Foo
{
private:
    int a_ = 0;
    int b_ = 0;
    int c_ = 0;
    int d_ = 0;

public:
    constexpr Foo() = default;

    constexpr Foo(int a, int b, int c, int d)
    : 
        a_{a}, b_{b}, c_{c}, d_{d}
    {}

    constexpr Foo& SetA(int i) { a_ = i; return *this; }
    constexpr Foo& SetB(int i) { b_ = i; return *this; }
    constexpr Foo& SetC(int i) { c_ = i; return *this; }
    constexpr Foo& SetD(int i) { d_ = i; return *this; }

    friend std::ostream& operator<<(std::ostream& os, const Foo& f)
    {
        return os << f.a_ << " " << f.b_ << " " << f.c_ << " " << f.d_ << " ";    
    }
};

int main() 
{
    constexpr Foo f = Foo{}.SetB(5).SetA(2);
    std::cout << f;
}

Live Example using Clang 3.4 SVN trunk with std=c++1y.

I'm not sure if classes with 30 parameters are a good idea (Single Responsiblity Principle and all that) but at least the above code scales linearly in the number of setters, with only 1 argument per setter. Note also that there are only 2 constructors: the default one (which takes its arguments from the in-class initializers) and the full one which takes 30 ints in your ultimate case).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top