【问题标题】:Compile-Time Named Parameter Idiom with constexpr带有 constexpr 的编译时命名参数惯用语
【发布时间】:2013-10-01 20:31:22
【问题描述】:

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

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

【问题讨论】:

  • 你必须设置的成员是否永远都是整数类型?
  • 好消息。这应该简化事情。今晚下班后我会尝试一下,我会告诉你的。
  • #SamCristall 嗨,Sam,很抱歉没有尽快回复您。我无法在一个晚上完成此操作,同时又忙于工作。无论如何,我会在几分钟内发布我的想法。

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


【解决方案1】:

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

Live Example 使用带有std=c++1y 的 Clang 3.4 SVN 中继。

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

【讨论】:

  • 我认为 C++1y 会解决这个问题——非常感谢你的帖子,但我认为我们还不能将我们的链切换到那个(这是用于生产带有自定义后端的固件。)另外,30 个参数可能听起来很多,但这些只是配置硬件的数据块,而硬件实际上要求它们是实心的块。
  • @SamCristall 如果这是用于固件,那么它是需要在其上运行的二进制代码,并且没有模板 stuf,对吗?在这种情况下,您可以简单地使用带有-std=c++1y 的 Clang 3.4 来编译和安装,并使用 Clang 3.3 或带有 std=c++11 的其他代码库(另请注意,Clang 3.4 的每晚构建是 @987654322 @ 用于 Ubuntu/Debian Linux)。
  • 我真的很想使用它,但是也有很多工具可以用于它,因为我们的 Clang/LLVM 实际上是一个分支(我们有自己的 16 位芯片) .我会尝试使用它,但与此同时,我很好奇在我们坚持使用 C++11 时是否有解决方法,所以我将问题留待解决。无论如何我都会 +1,因为这对于使用 C++1y 的人来说是一个很好的解决方案
  • @SamCritall 您未设置的成员(您未使用 SetX 明确设置的成员),它们是否始终是其类型的默认值,还是您真的希望能够使用自定义设置它们预先设置值并在为其他成员调用 Set 时保留这些值?
  • @SamCristall 经过一些实验,似乎无法避免没有 C++1y 的 30*30 缩放。问题是任何类内初始化程序 + 单参数构造函数都需要修改元组/数组数据成员,而这在 C++11 中是不允许的(在 constexpr 中是不允许的,在运行时很容易)。我用可变参数模板尝试了一些东西,但没有走得太远。最好等几个星期/几个月,直到 Clang 3.4 出来:-)
【解决方案2】:

使用模板元编程

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

示例类定义及其使用

下面是一个示例,说明您需要定义一个类:

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 的索引访问字段,但这不会阻止您使用友好名称包装设置器,就像上面的 SetPortNumber 和 SetPortLetter 所做的那样。 (他们只是转发到 Set 和 Set)

关于存储

在当前的实现中,在调用了所有的 setter 并声明了对象之后,这些字段最终被存储在基类中名为 dataconst unsigned char 的紧凑数组中。如果您使用非无符号字符的字段(如上面使用 PortNumber 所做的那样),则该字段分为大端 unsigned char (可以根据需要更改为小端)。如果您不需要具有实际内存地址的实际存储,则可以通过修改 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 实现一起使用。 它已经好几个小时都没有经过测试,所以当然可能存在错误,但它应该为您提供一个良好的开始基础。我希望这会有所帮助!

【讨论】:

  • 这很完美,比我目前使用的 C 预处理器宏要好得多。将其排除在内存之外的能力也是一个非常有趣的含义。谢谢凯文!
  • @SamCristall 在访问字段时要小心使用正确的索引,因为编译器可能不会警告您任何错误。还要确保字段在您的类顶部和 const_obj 基类中以相同的顺序放置,否则事情会混淆(我并没有真正进行任何检查)
  • 我不知道在哪里以及如何将 pack_field 从 unsigned chars 转换回 int (“Get”函数)。当您键入 cast 时会发生这种情况吗?如果是这样,这个实现会在小端 CPU 上工作吗?您注意到可能需要它,但我不确定为什么需要它。
  • @tower120 这些值被编码到类型本身。它们是在编译时从模板参数中读取的(即它们不是从程序中的内存位置读取的)。所以,是的,这也适用于小端。 packed_storage 最终将值存储在 big endien 中,但它仅在您需要在运行时访问变量的情况下才存在(在大多数用例中您不需要这样做)。
  • 有趣的是,可以扩展您的解决方案以支持字符串文字字段。使用 setter 看起来像 "MyStruct::SetString ()",而 getter 返回 "char []"那些领域?..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-09-09
  • 2021-10-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-28
  • 2020-08-29
相关资源
最近更新 更多