代码之家  ›  专栏  ›  技术社区  ›  Sam Cristall

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

  •  4
  • Sam Cristall  · 技术社区  · 12 年前

    我最近遇到过很多情况,命名参数习语会很有用,但我希望它在编译时得到保证。在链中返回引用的标准方法似乎总是调用运行时构造函数(使用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。如果这意味着不可能,那么我想事情就是这样。

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

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

    然而,大多数人甚至没有使用这个,只是输入了一团数字。因此,作为一种有效的改进,我想朝着这样的方向发展(因为它也迫使更好的代码):

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

    这可能要详细得多,但在以后阅读时要清楚得多。

    2 回复  |  直到 11 年前
        1
  •  4
  •   user955279 user955279    12 年前

    使用模板元编程

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

    一个示例类定义及其使用

    下面是一个例子,说明定义一个类需要什么:

    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所做的那样。(它们只是转发到“设置<0>”和“设置&lgt;1>)

    关于存储

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

    局限性

    这个实现只允许将积分类型用作字段(short、int、long、bool、char的所有类型)。不过,您仍然可以提供对多个字段执行操作的setter。例子:

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

    完整代码

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

    Full Implementation

    其他注意事项

    这段代码已经过测试,并与g++和clang的C++11实现一起工作。 它已经好几个小时没有经过测试了,所以当然可能会有漏洞,但它应该为你提供一个良好的基础。我希望这能有所帮助!

        2
  •  3
  •   TemplateRex    12 年前

    在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个参数。还要注意的是,只有两个构造函数:默认构造函数(从类内初始值设定项中获取参数)和完整构造函数(在最终情况下使用30 int)。