4

我最近遇到了很多命名参数习语很有用的情况,但我希望它在编译时得到保证。在链中返回引用的标准方法几乎总是调用运行时构造函数(使用 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) );

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

4

2 回答 2

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模板完成的。它提供了一种在编译时修改对象的机制。与 a 非常相似Tuple,使用基于 0 的索引访问字段,但这不会阻止您使用友好名称包装 setter,就像上面的 SetPortNumber 和 SetPortLetter 所做的那样。(他们只是转发到 Set<0> 和 Set<1>)

关于存储

在当前的实现中,在调用了所有的 setter 并声明了对象之后,字段最终被存储在基类中const unsigned char命名为 ' 的紧凑数组中。data如果您使用非无符号字符的字段(如上面使用 PortNumber 所做的那样),则该字段分为 big endien unsigned char(可以根据需要更改为 little endien)。如果您不需要具有实际内存地址的实际存储,则可以通过修改packed_storage(请参阅下面的完整实现链接)完全省略它,并且在编译时仍然可以访问这些值。

限制

此实现仅允许将整数类型用作字段(所有类型的 shorts、ints、longs、bool、char)。不过,您仍然可以提供作用于多个字段的 setter。例子:

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

完整代码

这个小库的完整代码可以在这里找到:

全面实施

补充说明

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

于 2013-10-02T03:01:04.143 回答
3

在 C++14 中,对constexpr函数的约束将被放宽,通​​常的引用返回设置器链接将在编译时工作:

#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;
}

使用Clang 3.4 SVN 主干和std=c++1y.

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

于 2013-09-26T09:43:37.417 回答