是否可以安全地引用具有与模板不兼容的模板参数的C ++模板类型?

时间:2019-04-28 08:48:58

标签: c++ templates language-lawyer

在下面的代码示例中,我定义了一个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 的匹配项返回。如果找不到针对特定地图类型的条目。如果以上代码有效,那么我认为我可以产生所需的行为。

2 个答案:

答案 0 :(得分:1)

不幸的是,我相信,该标准不能保证不会实例化std::map<DT, DT>[temp.inst]/1仅指定

  

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

请注意,这仅告诉我们何时可以确保模板被实例化,如果不能进行实例化,则不能保证模板不会被实例化。 [temp.inst]/10仅对此提供保证

  

[…]函数模板,变量模板,成员模板,非虚拟成员函数,成员类,类模板的静态数据成员或constexpr if语句的子语句([stmt。如果],则除非需要此类实例化。 […]

请注意,此列表中没有类模板。因此,我认为,即使认为没有必要,编译器理论上也可以实例化std::map<DT, DT>。如果使用std::map作为键和值类型实例化模板DT无效,那么您将遇到问题。我找不到有关使用不支持比较运算符的键类型实例化std::map的任何保证。尽管我希望这基本上可以与任何实现一起使用,但我确实认为从理论上讲允许实现,例如,使用static_assert检查密钥类型是否满足必要的要求。 [res.on.functions]/1似乎适用(重点是我):

  

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

因此,我认为,严格来说,该标准不能保证使用std::map<DT, DT>会起作用……

如果您只是想使用std::map<DT, DT>作为标记类型来表示特殊情况,我建议您不要使用std::map,而是使用其他一些东西,例如:

template <typename, typename>
struct default_config_tag;

然后以default_config_tag<DT, DT>或仅以DT作为标签(不确定是否需要将参数作为具有两个类型参数的模板的实例)就足够了。

答案 1 :(得分:0)

您已经got the answer to your question,但是对于本文的读者来说,问题的上下文也同样有趣,因此我认为值得一提的是,您可以在诸如以下的用例中使用标记分发您自己的:

  • 在编译时为特定类型(例如int)或类型组(例如std::map<K, V>(对于通用KV)设置默认配置字符串< / li>

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

例如:

#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
}

但是,这并不能解决您希望(基于对自己的帖子的评论)在运行时指定通用类型的后备默认配置字符串的值(例如,{{ 1}})。