【问题标题】:Safe to reference a C++ template type having a template parameter that's not compatible with the template?安全地引用具有与模板不兼容的模板参数的 C++ 模板类型?
【发布时间】:2019-04-28 08:48:58
【问题描述】:

在下面的代码示例中,我定义了一个类 DT(我的默认类型),我希望它能够作为任意模板的参数传递。在此示例中,我将 DT 作为 std::map 的键和值参数传递。我实际上并没有尝试实例化 map

,我只是想使用 map
作为模板化函数的模板参数(在此示例中,函数 f())从未实际引用类型——它仅用于生成函数的特定类型实例。 (请注意,您实际上不能实例化 std::map
,因为映射的键必须是可比较的,但 DT 不是。)
#include <iostream>
#include <map>

using namespace std;

class DT {};

template <typename T>
string f() {
    return "foo";
}

int main() {
    cout << f<map<DT,DT>>() << endl;
    return 0;
}

使用 g++ 似乎可以正常工作。我什至尝试为所有四个映射参数传递 DT(覆盖默认比较器和分配器类型)。仍然有效。但我担心这种技术可能会因其他模板或其他编译器而失败。所以我的问题是:对于任何符合 c++11 标准(及更高标准)的 c++ 编译器上的任何模板来说,这总是安全的。换句话说,将完全不兼容的类型作为模板的参数传递是否总是安全的,只要您从不尝试实例化该模板?

如果您想知道我到底为什么要做这样的事情,我正在尝试设置一个可以存储类型相关配置字符串的类。它将有这两种方法:

template<typename T>
const string& get<T>() const;

template<typename T>
void set<T>(const string& value);

它在很大程度上令我满意。它有几个不错的功能。例如,类型 int、const int、int&、const int& 等都被视为同一类型(这是我想要的)。如果没有找到更具体的派生类型的条目,您可以存储基类的配置字符串,以后可以通过派生类型检索该配置字符串。但是对于 std::map 的情况,我希望能够使用 map

类型存储默认配置字符串,该字符串稍后将作为任何 map 的匹配项返回当没有为手头的特定地图类型找到条目时。如果上面的代码是有效的,那么我想我可以产生想要的行为。

【问题讨论】:

  • 我知道这是语言律师标签,但为什么不使用您自己的标签类型而不是std::map
  • @super 我不明白这个问题。如果目标是将配置与任何可能的类型相关联,那么它也需要处理std:;map(因为这是可能的类型之一)。
  • @melpomene 问题指出std::map&lt;DT, DT&gt; 是不兼容的,因为它实际上无法使用,所以我认为它的目的是用唯一类型标记get/set
  • @super, @melpomene:是的,我只想通过调用config.set&lt;std::map&lt;DT,DT&gt;&gt;("foo") 将配置字符串绑定到类型std::map&lt;DT, DT&gt;。然后,如果有人在以前没有为类型std::map&lt;int,int&gt; 存储条目时调用config.get&lt;std::map&lt;int,int&gt;&gt;(),它将返回为std::map&lt;DT,DT&gt; 存储的值。我当然可以添加operator&lt;() 等以使 DT 成为有效密钥,但其他模板上的其他模板参数可能有其他我无法预料的要求。
  • @MatthewBusche 出于好奇,您的 get&lt;&gt;()set&lt;&gt;() 方法是包装对模板化单例(字符串)的访问,还是使用实例绑定配置动态扩展给定的 Config 实例基于 set&lt;&gt;()get&lt;&gt;() 调用不同类型的字符串?

标签: c++ templates language-lawyer


【解决方案1】:

不幸的是,我相信标准并不能保证std::map&lt;DT, DT&gt; 不会被实例化。 [temp.inst]/1 仅指定

除非已显式实例化或显式特化类模板特化,否则当在需要完全定义的对象类型的上下文中引用特化或类类型的完整性影响语义时,会隐式实例化类模板特化的程序。 […]

请注意,这仅告诉我们何时保证实例化模板,如果不需要实例化,它不保证不会实例化模板。 [temp.inst]/10只给了这样的保证

[…] 函数模板、变量模板、成员模板、非虚拟成员函数、成员类、类模板的静态数据成员或 constexpr if 语句的子语句([stmt. if]),除非需要这样的实例化。 […]

请注意此列表中缺少类模板。因此,我相信,理论上允许编译器实例化std::map&lt;DT, DT&gt;,即使认为没有必要这样做。如果使用DT 作为键和值类型来实例化模板std::map 将无效,那么您就会遇到问题。我找不到任何关于使用不支持比较运算符的键类型实例化 std::map 的保证。虽然我希望这基本上适用于任何实现,但我确实认为理论上允许实现,例如,有一个static_assert 来检查密钥类型是否满足必要的要求。 [res.on.functions]/1 似乎适用(强调我的):

在某些情况下(替换函数、处理函数、对用于实例化标准库模板组件的类型的操作),C++ 标准库依赖于 C++ 程序提供的组件。 如果这些组件不符合其要求,则标准对实施没有任何要求。

因此,我认为,严格来说,标准并不能保证使用std::map&lt;DT, DT&gt; 会起作用……

如果您只是想使用std::map&lt;DT, DT&gt; 作为一种标记类型来指示特殊情况,我建议不要使用std::map,而是使用其他东西,例如:

template <typename, typename>
struct default_config_tag;

然后default_config_tag&lt;DT, DT&gt; 或只是DT 作为您的标签(不确定您是否需要将参数作为具有两个类型参数的模板的实例)就足够了……

【讨论】:

  • 我想过,但我想要地图和多地图等的不同默认值。还有其他方法我可以采用,但如果我可以使用设置地图的默认值会很好与我针对特定类型所做的相同类型的模板。
  • 感谢您的详细回复。我可能会继续我的坏主意,看看我是否至少可以让这件事发挥作用。稍后我可以尝试重新设计界面以避免这个潜在的问题。
  • @MatthewBusche 你在这里尝试做的事情对我来说似乎很复杂。我不确定您的目标到底是什么,但我认为必须有一种更简单的方法可以实现这一目标。可能需要对整个问题采取完全不同的方法……但在不知道实际问题是什么的情况下不能说更多……
  • 详细说明整个问题对于本论坛来说是不合适的。反正我还没准备好分享。就我的配置类而言,到目前为止只有大约 150 行代码——所以一点也不复杂。但是我还没有放入这个模板的东西。一般来说,如果我想把它提升到第 n 级,这可能会变得复杂——支持 map&lt;DT,int&gt;map&lt;int,DT&gt; 的部分默认设置。或者更糟糕的map&lt;int,list&lt;DT&gt;&gt; 等设置。但我不打算提供如此精细的控制——至少一开始不会。
  • @MatthewBusche 我通常会质疑您是否真的需要支持 std:: 容器作为配置值。如果你想要一种递归/树状的配置结构,为什么不让一个配置节点包含其他配置节点呢?然后,您只需要允许作为值的任何类型的默认值以及默认配置节点,您的问题就应该得到解决!?至少,我已经这样做了很多年了,还没有让我失望……
【解决方案2】:

您已经got the answer to your question,但是对于这篇文章的读者来说,问题的上下文同样很有趣,所以我认为值得一提的是,您可以将标签调度用于您自己的用例, 到:

  • 在编译时为特定类型(例如int)或类型组(例如std::map&lt;K, V&gt; 用于通用KV)设置默认配置字符串

如果没有标签分派,这可能会很棘手,因为您可能不会部分专门化函数模板。

例如:

#include <map>
#include <string>
#include <iostream>

template <typename T>
class Config {
 public:
  static const std::string& get() { return Config::getString(); }

  static void set(const std::string& value) { Config::getString() = value; }

  Config(Config const&) = delete;
  void operator=(Config const&) = delete;

 private:
  static std::string& getString() {
    static std::string s(defaultString(dispatch_tag<T>{}));
    return s;
  }

  template <typename U>
  struct dispatch_tag {};

  // Default string unless specified for specific types below.
  template <typename U = T>
  static constexpr std::string_view defaultString(dispatch_tag<U>) {
    return "default";
  }

  // Default config strings for a select number of types.
  static constexpr std::string_view defaultString(dispatch_tag<int>) {
    return "default int";
  }

  template <typename K, typename V>
  static constexpr std::string_view defaultString(
      dispatch_tag<std::map<K, V>>) {
    return "default map";
  }
};

int main() {
  std::cout << Config<int>::get() << "\n";                 // default int
  std::cout << Config<std::string>::get() << "\n";         // default
  std::cout << Config<std::map<int, int>>::get() << "\n";  // default map

  Config<int>::set("custom int");
  Config<std::map<int, int>>::set("custom int-int map");

  std::cout << Config<int>::get() << "\n";                  // custom int
  std::cout << Config<std::map<int, int>>::get() << "\n";   // custom int-int map
  std::cout << Config<std::map<int, char>>::get() << "\n";  // default map
}

但是,这并不能解决您希望(基于您自己的帖子的 cmets)在运行时指定泛型类型的后备默认配置字符串的值(例如,@987654327 @)。

【讨论】:

    猜你喜欢
    • 2011-07-26
    • 1970-01-01
    • 2018-06-25
    • 1970-01-01
    • 1970-01-01
    • 2023-01-25
    • 1970-01-01
    • 2011-10-01
    • 1970-01-01
    相关资源
    最近更新 更多