【问题标题】:c++ runtime dispatch of 2 sets of static constants2组静态常量的c ++运行时调度
【发布时间】:2019-12-20 18:28:08
【问题描述】:

动机

一个 CLI 应用程序尝试使用 box drawing characters。但根据标准输出设备,这可能不合适,因此有一个-t 选项来使用纯 ASCII 文本。

这个概念只是一个例子,它适用于我们想要在运行时从两组或更多组静态常量中进行选择的任何地方。

问题是什么是合理的 c++ 技术来干净利落地促进这一点。

所需的使用语法

这只是指示性的,但不断的“调用”应该很简洁

int main(int argc, char* argv[]) {

  // using derived class style here, but anything would do
  auto t = std::strcmp(argv[1], "-t") == 0 ? TxtTerminal() : BdTerminal();

  // here we want the syntax to be short
  std::cout << "text" << t.horiz << t.right_t << '\n';

}

已经尝试过

我已经尝试过 Base 和 Derived 类样式(如上),但存在一些问题。我不能在TxtTerminal 中放入一组常量,在BdTerminal 中放入一组常量(扩展TxtTerminal)。

  • 使用静态常量不起作用,因为 C++ 不支持“后期静态绑定”,所以它总是使用您拥有实例的任何类的常量。
  • 使用成员变量和这些类的正确实例(如上所示)也不起作用(很好),因为 Derived 类无法直接初始化 Base 类成员....
  • 也不是通过它的构造函数初始化列表...
  • 派生类必须将整个集合传递给基类构造函数,然后基类构造函数修改已经(BaseClass)初始化的值。真是尴尬又啰嗦。我想通过一些 struct config { .. }; 可能会起作用
  • 类似地使用单个类,将两组常量放在map&lt;key,config&gt; 中,并使用getter 通过构造函数初始化bool textmode 成员状态变量来检索适当的常量。
  • 也许使用带有模板参数的模板化解决方案?不,因为这是一个运行时开关。
  • 类似于模板的想法将是某种解决方案,其中using 别名用于 2 个单独的常量命名空间..同样的问题..编译时间..不是运行时。
  • 使用 variable templates 有点像 new c++20 maths constants 。同样的问题,不是运行时。

当然,所有这些const,即“画框终端”的控制字符字符串,每个都只是一个字节长。理想情况下,这些只是在可执行文件的.text (linux) 段中作为字面初始化的const char* 压缩在一起(std::string 似乎有点矫枉过正)。无论我们在哪里编写这些常量,我们都希望使用简单的concatenation 来使代码具有可读性和可维护性。 const char* 字符串文字不是那么容易吗?

许多选项,没有一个看起来很棒。我错过了一些明显的东西吗?

编辑为了回答下面的问题以及@walnut 最初的answer,这里有一些使用“静态非实例”方法的更充实的代码。

其中一些未解决的问题,见cmets:


// NOTE this code compiles and runs, but doesn't quite do what we need
// see comments

#include <cstring>
#include <iostream>

struct TextMode {
  static inline const auto cls     = "TM cls";
  static inline const auto right_t = "TM right_t";
  static inline const auto left_t  = "TM left_t";
};

struct BlockDrawMode {
  static inline const auto cls     = "BD cls";
  static inline const auto right_t = "BD right_t";
  static inline const auto left_t  = "BD left_t";
};

struct Terminal {

  Terminal(bool textmode = true) { mode = textmode ? &text_mode : &block_drawing_mode; }

  // what is this type? some parent class of TextMode/BlockDrawMode?!
  // then we get the initilisation loop again...
  //    ???????    // could be a reference initilised in cstr initilisation, but that's a detail
  const TextMode* mode;

  static inline const auto text_mode          = TextMode{};
  static inline const auto block_drawing_mode = TextMode{};
  // obviously this needs to be ..              ^ BlockDrawMode
  // but then doesn't compile because there is no class hierarchy ...

};

int main(int argc, char* argv[]) {
  Terminal t{strcmp(argv[1], "-t") == 0};

  std::cout << t.mode->cls << '\n' << t.mode->right_t << '\n';

  // output  (-t makes no difference right now, due to the above issues)
  // TM cls
  // TM right_t

  return 0;
}

EDIT2:我添加了一个self answer below,它使用aggregate initialisation 来避免很多不必要的继承并发症。感觉有点“脏”,但看起来干净且工作正常?

【问题讨论】:

  • argv[1] == "-t"总是为假,因为您比较两个永远不会相等的 指针(并且只有指针本身)。要比较 C 风格的以 null 结尾的字符串,请使用 std::strcmp。您应该始终首先检查argc,以确保存在相应的argv 元素。
  • @Someprogrammerdude 当然,但这不是重点,它只是说明性的。我会在问题中解决它
  • 目前还不清楚是什么问题。您直接进入了您尝试的内容,而没有解释实际问题。如果没有这些,这些尝试就会显得断章取义。请提供minimal reproducible example,因为不清楚TxtTerminalBdTerminal 是什么,以及为什么当前(建议的)解决方案不适合您。
  • 关于“......派生类不能直接初始化基类成员”和“派生类不能直接初始化基类成员”虽然这是真的,你可以 在派生类构造函数初始化列表中调用合适的基类构造函数:Derived() : Base() {}(当然也可以传递参数)。如果您只想“重用”基类构造函数,那么“使用”它:class Derived : public Base { public: using Base::Base; ... };
  • 在我看来,您唯一的问题是提供不同的常量以在 Base/Derived 类中使用,但这很容易通过为这些常量使用虚拟 getter 来解决方法。

标签: c++ static runtime constants dispatch


【解决方案1】:

从我收集到的 cmets 中,您正在寻找的是: 如果在需要虚拟接口或继承的行为上没有任何其他差异,那么您只需定义一个指针成员,该成员在构造时为正确的实现选择:

struct Terminal {

    Terminal(/* parameters */) {
        chooseConfig(/* some arguments */);
    }

    static constexpr char config1[]{/*...*/};
    static constexpr char config2[]{/*...*/};

    const char* config;

    void chooseConfig(/*arguments*/) {
        config = /*condition*/ ? config1 : config2;
    };

    // use `config` everywhere

};

int main(int argc, char* argv[]) {

    Terminal terminal{/* arguments */};

    //...
}

如果这个类的所有实例都应该共享相同的配置并且每次传递参数来选择配置太麻烦,那么你也可以将configchooseConfigstatic改为static而不是调用它在构造函数中,在mainTerminal::chooseConfig(/*arguments*/); 中调用一次。

【讨论】:

  • 是的,使用虚拟调度解决了我遇到的一些挑战。即如何初始化与我们的 Base() 和 Derived() 互相争斗。我们仍在将.textstatic 内容转换为“运行时成员变量”Base::config
  • 是的!每个都有一个“实例”(即实际上没有)。没有任何意义了。你刚刚添加的第二件事!这更像是我“觉得”应该是可能的,但不知道如何表达......我会玩的。
  • @OliverSchonrock 这只是一个指针。访问它与直接访问数组一样快,除了常量折叠优化。第二种方法与第一种方法具有几乎相同的性能。
  • 我尝试使用static 方法并将代码放在我的问题末尾。在我最初尝试这条路线时,我遇到了类似的事情......似乎无法摆脱继承?
  • 与你的相比,我的方法在“继承杂草”中得到了很多,因为你的只是一个 char* 数组,而我的是“标记”结构。我需要它,请参阅 main 中的用法。 std::map 或类似的将需要 class:enum 的键......它级联......没有办法保持简单?
【解决方案2】:

建议的“自我回答”:

这是一个避免config structs 的层次结构复杂性的版本。基本上是aggregate initialization 来救援...

这个最终版本显示了一些正确的终端控制字符串和一些使用ref to static inline struct 的调整。加上它使用designated initialisers。请注意,如果需要,可以从 Base Struct Config 详细信息中进行选择性覆盖。所有的“键”在编译时都会自动检查,所以在维护过程中防止错误。

struct Mode {
  const char* const esc           = "";
  const char* const cls           = "";
  const char* const bd_on         = "";
  const char* const bd_off        = "";
  const char* const underline_on  = "";

  const char* const black   = "";
  const char* const red     = "";
  const char* const green   = "";
  const char* const yellow  = "";
  const char* const blue    = "";
  const char* const magenta = "";
  const char* const cyan    = "";
  const char* const white   = "";

  const char* const reset   = "";

  const char        horiz         = '-';
  const char        vert          = '|';
  const char        right_t       = '|';
  const char        left_t        = '|';
  const char        bottom_t      = '|';
  const char        top_t         = '|';
  const char        intersec      = '|';
};

struct Terminal {
  static inline const Mode text_mode;
  // warning: this is a C99 extension until C++20 comes in
  // but it nicely compile checks and self documents the code
  // supported by gcc4.7, clang3.0 and msvc19.21
  static inline const Mode box_draw_mode{
      .esc           = "\x1b",
      .cls           = "\x1b[2J",
      .bd_on         = "\x1b(0",
      .bd_off        = "\x1b(B",
      .underline_on  = "\x1b[4m",

      .black   = "\x1b[30m",
      .red     = "\x1b[31m",
      .green   = "\x1b[32m",
      .yellow  = "\x1b[33m",
      .blue    = "\x1b[34m",
      .magenta = "\x1b[35m",
      .cyan    = "\x1b[36m",
      .white   = "\x1b[37m",

      .reset   = "\x1b[0m",

      .horiz    = '\x71',
      .vert     = '\x78',
      .right_t  = '\x75',
      .left_t   = '\x74',
      .bottom_t = '\x76',
      .top_t    = '\x77',
      .intersec = '\x6e',
  };

  const Mode& m;
  Terminal(bool textmode = true) : m{textmode ? text_mode : box_draw_mode} {}

};


int main(int argc, char* argv[]) {

  Terminal t1{true};
  Terminal t2{false};

  // usage
  // std::cout << t1.m.cls << '\n' << t1.m.right_t << '\n';
  // std::cout << t2.m.cls << '\n' << t2.m.right_t << '\n';

  return 0;
}

这是一个godbolt(请注意,clang>3 和 gcc>4.7 已经很好地编译了指定的初始化程序)。另请注意,godbolt 显示在-O3 上,初始化程序得到优化,以至于char 值刚刚成为内联寄存器加载。在 O1 上,我们可以清楚地看到在 .text 段中布置的字符串,并且在 Terminal 构造期间发生的唯一事情是为 Mode&amp; m 设置单个指针以指向两个结构之一:mov qword ptr [rdi], rcx .好吗?

【讨论】:

    猜你喜欢
    • 2017-11-15
    • 2010-10-24
    • 2012-07-14
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多