使用constexpr编译时命名参数成语

时间:2013-09-26 03:06:23

标签: c++ c++11 constexpr c++14 named-parameters

我最近遇到过很多情况,其中命名参数成语很有用,但我希望它能在编译时得到保证。返回链中引用的标准方法几乎总是会调用运行时构造函数(使用Clang 3.3 -O3进行编译)。

我无法找到任何参考此内容的内容,因此我尝试将其与constexpr配合使用并获得了一些功能:

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

虽然这对于少量参数是可以接受的,但对于较大的数字,它很快就会变得混乱:

    //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); }

有更好的方法吗?我正在考虑使用具有许多(30+)参数的类来执行此操作,如果将来扩展,这似乎很容易出错。

编辑:删除了C ++ 1y标签 - 而C ++ 1y似乎确实解决了这个问题(感谢TemplateRex!)这是生产代码,我们坚持使用C ++ 11。如果这意味着它是不可能的,那么我想这就是它的方式。

EDIT2:为了说明我为什么要这样做,这是一个用例。目前使用我们的平台,开发人员需要明确设置硬件配置的位向量,虽然这是可以的,但它非常容易出错。有些人正在使用C99扩展中的指定初始值设定项,这是可以的但非标准的:

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

然而,大多数人甚至没有使用它,只是输入了一大堆数字。因此,作为一项工作改进,我想转向这样的事情(因为它也会强制更好的代码):

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

这可能更冗长,但稍后阅读会更清楚。

2 个答案:

答案 0 :(得分:4)

使用模板元编程

我想出了解决问题的方法(至少部分)。通过使用模板元编程,您可以利用编译器为您完成大部分工作。对于那些以前从未见过这样的代码的人来说,这些技术看起来很奇怪,但幸运的是,大多数复杂性都可以隐藏在标题中,用户只能以简洁的方式与库进行交互。

样本类定义及其使用

以下是您需要定义类的示例:

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>();
    }
};

使用班级

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

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

大部分工作都是由const_obj模板完成的。它提供了一种在编译时修改对象的机制。与Tuple非常相似,可以使用基于0的索引访问字段,但这并不能阻止您使用友好名称包装setter,就像上面的SetPortNumber和SetPortLetter一样。 (他们只是转发到Set&lt; 0&gt;和Set&lt; 1&gt;)

关于存储空间

在当前实现中,在调用所有setter并声明对象之后,这些字段最终存储在基类中名为const unsigned char的{​​{1}}的紧凑数组中。如果您使用的字段不是无符号字符(例如上面用PortNumber执行的字段),则字段将分为大端data(可根据需要更改为小端)。如果您不需要具有实际内存地址的实际存储,则可以通过修改unsigned char(请参阅下面的完整实现链接)完全省略它,并且仍然可以在编译时访问这些值。

<强>限制

此实现仅允许将整数类型用作字段(所有类型的short,int,long,bool,char)。您仍然可以提供作用于多个字段的setter。例如:

packed_storage

完整代码

可以在此处找到实现此小库的完整代码:

Full Implementation

附加说明

此代码已经过测试,可以与g ++和clang的C ++ 11实现一起使用。 它没有经过几小时和几小时的测试,因此当然可能存在错误,但它应该为您提供良好的基础。我希望这有帮助!

答案 1 :(得分:3)

在C ++ 14中,constexpr函数的约束将被放宽,引用返回setter的常规链接将在编译时起作用:

#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使用Clang 3.4 SVN中继与std=c++1y

我不确定具有30个参数的类是否是一个好主意(单一责任原则和所有这些),但至少上面的代码在setter数量上线性扩展,每个setter只有1个参数。另请注意,只有2个构造函数:默认的构造函数(从类内初始化程序中获取其参数)和完整的构造函数(在最终情况下需要30个整数)。