【问题标题】:constexpr and initializationconstexpr 和初始化
【发布时间】:2011-12-23 00:53:48
【问题描述】:

也许已经有人问过类似的问题,当然,这是一个挑剔...

我有一堆常量 std::maps 可以在 enum (class) 值和它们的 std::string 表示之间切换(两种方式)。这里有人向我指出,这些映射将在运行时初始化,当其他初始化代码运行时,在我的程序执行所有好东西之前。这意味着常量表达式会影响运行时性能,因为映射是由它们的枚举字符串对构建的。

作为说明性示例,以下是其中一张地图的示例:

enum class os
{
    Windows,
    Linux,
    MacOSX
};
const map<string, os> os_map =
     { {"windows", os::Windows},
       {"linux",   os::Linux},
       {"mac",     os::MacOSX} };
const map<os, string> os_map_inverse =
     { {os::Windows, "windows"},
       {os::Linux,   "linux"},
       {os::MacOSX,  "mac"} };

C++11 constexpr 会对性能产生任何影响,还是我对运行时初始化惩罚的假设是错误的?我认为编译器可以将常量 STL 容器作为纯数据嵌入到可执行文件中,但显然这可能不像我说的那么容易?

【问题讨论】:

  • 你为什么不试试boost::bimap 来实现枚举和它的字符串表示之间的双边映射呢?添加新值时出错的可能性大大降低。
  • Xeo:为这么简单的事情引入 Boost?不,谢谢,我没有依赖关系,并且真的很想保持这种状态;)...我什至可以用unordered_map 替换字符串-> 枚举映射,用@987654329 替换枚举->字符串映射@(枚举值并不重要,它们只是一个一个地计数)以提高性能。 boost::bimap 相比之下会很糟糕:)
  • @rubenvb :然而Boost.MultiIndex 可以做到这一点,更简洁,开销为 0。请不要将 Boost 视为“依赖项”。

标签: c++ initialization constants c++11


【解决方案1】:

我最近找到了提供枚举的最佳方式。见代码:

enum State {
    INIT, OK, ENoFou, EWroTy, EWroVa
};

struct StateToString {
    State state;
    const char *name;
    const char *description;
public:
    constexpr StateToString(State const& st)
            : state(st)
            , name("Unknown")
            , description("Unknown")
    {
        switch(st){
            case INIT  : name = "INIT"  , description="INIT";              break;
            case OK    : name = "OK"    , description="OK";                break;
            case ENoFou: name = "ENoFou", description="Error: Not found";  break;
            case EWroTy: name = "EWroTy", description="Error: Wrong type"; break;
            case EWroVa: name = "EWroVa", description="Error: Wrong value, setter rejected"; break;
            // Do not use default to receive a comile-time warning about not all values of enumeration are handled. Need `-Wswitch-enum` in GCC
        }
    }
};

将此示例扩展为一些通用代码,我们将其命名为 lib 代码:

/// Concept of Compile time meta information about (enum) value.
/// This is the best implementation because:
///   - compile-time resolvable using `constexpr name = CtMetaInfo<T>(value)`
///   - enum type can be implemented anywhere
///   - compile-time check for errors, no hidden run-time surprises allowed - guarantied by design. 
///   - with GCC's `-Wswitch` or `-Werror=switch` - you will never forget to handle every enumerated value
///   - nice and simple syntaxis `CtMetaInfo(enumValue).name`
///   - designed for enums suitable for everything
///   - no dependencies
/// Recommendations:
///   - write `using TypeToString = CtMetaInfo<Type>;` to simplify code reading
///   - always check constexpr functions assigning their return results to constexpr values
/**\code

    template< typename T >
    concept CtMetaInfo_CONCEPT {
        T value;
        const char *name;
        // Here could add variables for any other meta information. All vars must be filled either by default values or in ctor init list
    public:
        ///\param defaultName will be stored to `name` if constructor body will not reassign it
        constexpr CtMetaInfoBase( T const& val, const char* defaultName="Unknown" );
    };

   \endcode
 */

/// Pre-declare struct template. Specializations must be defined.
template< typename T >
struct CtMetaInfo;

/// Template specialization gives flexibility, i.e. to define such function templates
template <typename T>
constexpr const char* GetNameOfValue(T const& ty)
{ return CtMetaInfo<T>(ty).name; }

用户必须为用户类型定义专业化:

/// Specialization for `rapidjson::Type` 
template<>
struct CtMetaInfo<Type> {
    using T = Type;
    T value;
    const char *name;
public:
    constexpr CtMetaInfo(T const& val)
            : value(val)
            , name("Unknown")
    {
        switch(val){
            case kNullType                 : name = "Null"  ; break;
            case kFalseType: case kTrueType: name = "Bool"  ; break;
            case kObjectType               : name = "Object"; break;
            case kArrayType                : name = "Array" ; break;
            case kStringType               : name = "String"; break;
            case kNumberType               : name = "Number"; break;
        }
    }
    static constexpr const char* Name(T const& val){
        return CtMetaInfo<Type>(val).name;
    }
};

/// TEST
constexpr const char *test_constexprvalue = CtMetaInfo<Type>(kStringType).name;
constexpr const char *test_constexprvalu2 = GetNameOfValue(kNullType);

【讨论】:

    【解决方案2】:

    问题不在于初始化的性能,而在于初始化的顺序。如果有人在main 开始之前使用您的地图(例如在初始化命名空间范围变量时),那么您就是 SOL,因为您不能保证您的地图在用户初始化使用它之前已经初始化。

    但是您可以在编译时执行此操作。字符串文字是常量表达式,枚举数也是。一个简单的线性时间复杂度结构

    struct entry {
      char const *name;
      os value;
    };
    
    constexpr entry map[] = {
      { "windows", os::Windows },
      { "linux", os::Linux },
      { "mac", os::Mac }
    };
    
    constexpr bool same(char const *x, char const *y) {
      return !*x && !*y ? true : (*x == *y && same(x+1, y+1));
    }
    
    constexpr os value(char const *name, entry const *entries) {
      return same(entries->name, name) ? entries->value : value(name, entries+1);
    }
    

    如果您在常量表达式上下文中使用value(a, b),并且您指定的名称不存在,您将收到编译时错误,因为函数调用将变为非常量。

    要在非常量表达式上下文中使用value(a, b),您最好添加安全功能,例如向数组添加结束标记并在value 中抛出异常,如果您点击结束标记(函数调用仍将是一个常量表达式,只要你从不打结束标记)。

    【讨论】:

    • 似乎它不起作用(GCC 4.5.1):ideone.com/w8QFN。您认为这是编译器问题吗?
    • @atzz 是的,这是编译器问题。试试 GCC4.6。
    • 约翰内斯,感谢您的回复;我会的,明天。目前没有可用的编译器。
    • 使用 GCC4.6,它可以按预期工作,实际上 gcc.gnu.org/projects/cxx0x.html 表明 constexpr 支持是在 4.6 中添加的。约翰内斯,对这一切来回感到抱歉;我不应该在周日晚上涉足 C++ ;)
    • !*x &amp;&amp; !*y 对两个 nullptr 都返回 true。正确的?那么为什么true?什么应该返回nullptr==nullptr?至于我false。而且应该还有if( x==y )简单优化。
    【解决方案3】:

    啊,是的,这是一个典型的问题。

    我发现避免这种运行时初始化的唯一替代方法是使用纯 C 结构。如果您愿意加倍努力并将值存储在纯 C 数组中,则可以获得静态初始化(并减少内存占用)。

    这是 LLVM/Clang 实际使用 tblgen 实用程序的原因之一:普通表是静态初始化的,您可以按排序生成它们。

    另一种解决方案是创建一个专用函数:对于枚举到字符串的转换,很容易使用开关并让编译器将其优化为一个普通的表,对于字符串枚举它有点复杂(你需要if/else 分支组织正确以获得 O(log N) 行为)但对于小型枚举,线性搜索无论如何都一样好,在这种情况下,单个宏黑客(基于 Boost Preprocessor 的优点)可以为您提供所需的一切。

    【讨论】:

      【解决方案4】:

      constexpr 不适用于任意表达式,尤其不适用于将使用 freestore 的东西。 map/string 将使用 freestore,因此 constexpr 将无法在编译时初始化它们,并且在运行时没有代码运行。

      至于运行时损失:取决于这些变量的存储持续时间(我假设这里是静态的,这意味着在 main 之前初始化),您甚至无法衡量损失,尤其是在使用的代码中它们,假设您将多次使用它们,其中查找比初始化具有更多的“开销”。

      但至于一切,请记住规则一:让事情顺利进行。轮廓。做事快。按此顺序。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-12-14
        • 2012-11-10
        • 2018-04-27
        • 2015-04-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多