【问题标题】:Is it possible to perform a string to int mapping at compile time?是否可以在编译时执行字符串到 int 的映射?
【发布时间】:2015-06-30 23:59:35
【问题描述】:

是否可以在编译时执行唯一字符串到 int 的映射? 假设我有一个这样的模板用于分析:

template <int profilingID>
class Profile{
public:
    Profile(){ /* start timer */ }
    ~Profile(){ /* stop timer */ }
};

我把它放在函数调用的开头,如下所示:

void myFunction(){
    Profile<0> profile_me;

    /* some computations here */
}

现在我正在尝试执行以下操作,which is not possible since string literals cannot be used as a template argument

void myFunction(){
    Profile<"myFunction"> profile_me; // or PROFILE("myFunction")

    /* some computations here */
}

我可以声明全局变量来解决这个问题,但我认为避免以前的声明会更优雅。表单的简单映射

  • “我的功能”→0
  • “myFunction1”→1
  • “myFunctionN”→N

就足够了。但到目前为止,无论是使用 constexpr、模板元编程还是宏,我都找不到完成这种映射的方法。有什么想法吗?

【问题讨论】:

  • 您可能会使用预处理器技巧,或者在构建时生成一些(例如 C++ 头文件)文件。
  • 构建时生成秒。
  • 我真的不明白你为什么要在这里使用模板开始。与其为每个要分析的函数创建一个类,不如创建一个实例,将分析 ID 名称作为构造函数参数传入?或者还有其他一些模板参数/特化吗?
  • enum 不够映射吗?类似:enum { myFunction, myFunction1, ..., myFunctionN };。您可以将其用作模板参数——甚至不必双引号 :)
  • 如果字符串都像myFunctionN这样简单,只需去掉myFunction部分并将剩余的转换为int

标签: c++


【解决方案1】:

正如@harmic 在 cmets 中已经提到的,您可能应该只将名称传递给构造函数。这也可能有助于减少代码膨胀,因为您不会为每个函数生成新类型。

但是,我不想错过展示在字符串无法传递给构造函数的情况下可能有用的肮脏技巧的机会。如果您的字符串具有在编译时已知的最大长度,您可以将它们编码为整数。在以下示例中,我仅使用一个整数,它将系统上的最大字符串长度限制为 8 个字符。将方法扩展到多个整数(拆分逻辑方便地被一个小宏隐藏)留给读者作为练习。

代码利用 C++14 特性在constexpr 函数中使用任意控制结构。在 C++11 中,您必须将 wrap 编写为稍微不那么直接的递归函数。

#include <climits>
#include <cstdint>
#include <cstdio>
#include <type_traits>

template <typename T = std::uintmax_t>
constexpr std::enable_if_t<std::is_integral<T>::value, T>
wrap(const char *const string) noexcept
{
  constexpr auto N = sizeof(T);
  T n {};
  std::size_t i {};
  while (string[i] && i < N)
    n = (n << CHAR_BIT) | string[i++];
  return (n << (N - i) * CHAR_BIT);
}

template <typename T>
std::enable_if_t<std::is_integral<T>::value>
unwrap(const T n, char *const buffer) noexcept
{
  constexpr auto N = sizeof(T);
  constexpr auto lastbyte = static_cast<char>(~0);
  for (std::size_t i = 0UL; i < N; ++i)
    buffer[i] = ((n >> (N - i - 1) * CHAR_BIT) & lastbyte);
  buffer[N] = '\0';
}

template <std::uintmax_t Id>
struct Profile
{
  char name[sizeof(std::uintmax_t) + 1];

  Profile()
  {
    unwrap(Id, name);
    std::printf("%-8s %s\n", "ENTER", name);
  }

  ~Profile()
  {
    std::printf("%-8s %s\n", "EXIT", name);
  }
};

可以这样使用:

void
function()
{
  const Profile<wrap("function")> profiler {};
}

int
main()
{
  const Profile<wrap("main")> profiler {};
  function();
}

输出:

ENTER    main
ENTER    function
EXIT     function
EXIT     main

【讨论】:

    【解决方案2】:

    原则上可以。但是,我怀疑任何选择是否实用。

    您可以将您的键类型设置为constexpr 值类型(这不包括std::string),初始化您实现的值类型也不是问题,只需从数组中放入constexpr 构造函数字符。但是,您还需要实现constexpr 映射或哈希表和constexpr 哈希函数。实现constexpr 映射是困难的部分。还是可以的。

    【讨论】:

      【解决方案3】:

      你可以创建一个表:

      struct Int_String_Entry
      {
        unsigned int id;
        char *       text;
      };
      
      static const Int_String_Entry my_table[] =
      {
        {0, "My_Function"},
        {1, "My_Function1"},
        //...
      };
      const unsigned int my_table_size =
          sizeof(my_table) / sizeof(my_table[0]);
      

      也许你想要的是一个带有函数指针的查找表。

      typedef void (*Function_Pointer)(void);
      struct Int_vs_FP_Entry
      {
        unsigned int func_id;
        Function_Point p_func;
      };
      
      static const Int_vs_FP_Entry func_table[] =
      {
        { 0, My_Function},
        { 1, My_Function1},
        //...
      };
      

      为了更完整,您可以将所有三个属性组合到另一个结构中并创建另一个表。

      注意:由于表被声明为“静态常量”,它们在编译时被组装。

      【讨论】:

      • 感谢您的回答。但这些仍然是我想避免的声明。不是为了性能,而是为了美观。
      【解决方案4】:

      为什么不直接使用枚举:

      enum ProfileID{myFunction = 0,myFunction1 = 1, myFunction2 = 2 };
      

      ?

      你的字符串不会在运行时加载,所以我不明白这里使用字符串的原因。

      【讨论】:

      • 这将是我想避免的附加声明之一。另外我不想污染全局命名空间。
      【解决方案5】:

      这是一个有趣的问题。

      可以按如下方式静态初始化 std::map:

      static const std::map<int, int> my_map {{1, 2}, {3, 4}, {5, 6}};
      

      但我知道这样的初始化不是你想要的,所以我在看了你的例子后采取了另一种方法。

      全局注册表保存函数名称(std::string)和运行时间(std::size_t 表示毫秒数)之间的映射。

      构造一个AutoProfiler,提供函数名,它会记录当前时间。销毁时(将在我们退出函数时发生),它将计算经过的时间并将其记录在全局注册表中。

      当程序结束时,我们打印地图的内容(为此我们使用std::atexit 函数)。

      代码如下:

      #include <cstdlib>
      #include <iostream>
      #include <map>
      #include <chrono>
      #include <cmath>
      
      using ProfileMapping = std::map<std::string, std::size_t>;
      
      ProfileMapping& Map() {
        static ProfileMapping map;
        return map;
      }
      
      void show_profiles() {
        for(const auto & pair : Map()) {
          std::cout << pair.first << " : " << pair.second << std::endl;
        }
      }
      
      class AutoProfiler {
       public:
        AutoProfiler(std::string name)
            : m_name(std::move(name)),
              m_beg(std::chrono::high_resolution_clock::now()) { }
        ~AutoProfiler() {
          auto end = std::chrono::high_resolution_clock::now();
          auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(end - m_beg);
          Map().emplace(m_name, dur.count());
        }
       private:
        std::string m_name;
        std::chrono::time_point<std::chrono::high_resolution_clock> m_beg;
      };
      
      void foo() {
        AutoProfiler ap("foo");
        long double x {1};
        for(std::size_t k = 0; k < 1000000; ++k) {
          x += std::sqrt(k);
        }
      }
      
      void bar() {
        AutoProfiler ap("bar");
        long double x {1};
        for(std::size_t k = 0; k < 10000; ++k) {
          x += std::sqrt(k);
        }
      }
      
      void baz() {
        AutoProfiler ap("baz");
        long double x {1};
        for(std::size_t k = 0; k < 100000000; ++k) {
          x += std::sqrt(k);
        }
      }
      
      int main() {
        std::atexit(show_profiles);
      
        foo();
        bar();
        baz();
      
      }
      

      我编译为:

      $ g++ AutoProfile.cpp -std=c++14 -Wall -Wextra
      

      并获得:

      $ ./a.out
      bar : 0
      baz : 738
      foo : 7
      

      您不需要-std=c++14,但至少需要-std=c++11

      我知道这不是您想要的,但我喜欢您的问题并决定投入我的 0.02 美元。

      请注意,如果您使用以下定义:

      using ProfileMapping = std::multi_map<std::string, std::size_t>;
      

      您可以记录每个对每个函数的访问(而不是在写入第一个条目后放弃新结果,或覆盖旧结果)。

      【讨论】:

      • 就像你说的,这不是我想要的。不过你的回答很有趣。它确实避免了我在问题中提到的那种额外声明。如果没有地图查找开销,那就完美了。
      • 请注意,时间不会被任何地图查找污染。构造函数首先构建字符串,然后才记录开始时间(ergo 字符串构建不是计时的一部分)。类似地,析构函数首先记录结束时间,然后(在记录之后)才计算持续时间并插入到地图中:时间不会被任何地图开销污染。
      【解决方案6】:

      您可以执行类似于以下的操作。这有点尴尬,但可能比映射到整数更直接一点:

      #include <iostream>
      
      template <const char *name>
      class Profile{
      public:
          Profile() {
              std::cout << "start: " << name << std::endl;
          }
          ~Profile() {
              std::cout << "stop: " << name << std::endl;
          }
      };
      
      
      constexpr const char myFunction1Name[] = "myFunction1";
      
      void myFunction1(){
          Profile<myFunction1Name> profile_me;
      
          /* some computations here */
      }
      
      int main()
      {
          myFunction1();
      }
      

      【讨论】:

      • 这将是我想避免的附加声明之一。
      猜你喜欢
      • 1970-01-01
      • 2013-04-09
      • 2013-04-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-09
      • 1970-01-01
      • 2010-11-03
      相关资源
      最近更新 更多