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

Safe to reference a C++ template type having a template parameter that's not compatible with the template?

本文关键字:不兼容 C++ 类型 参数 安全 引用 是否      更新时间:2023-10-16

在下面的代码示例中,我定义了一个类DT(我的默认类型),我希望能够将其作为任意模板的参数传递。 在此示例中,我将 DT 作为 std::map 的键值参数传递。我实际上从未尝试实例化 map<DT,DT>,我只想使用 map<DT,DT> 作为模板化函数的模板参数(在此示例中为函数 f()),该函数从未实际引用过该类型——它仅用于生成特定于类型的函数实例。 (请注意,您实际上无法实例化 std::map<DT,DT>因为映射的键必须是可比的,但 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<DT,DT> 类型存储默认配置字符串,稍后将作为任何 map<Key,Value> 的匹配项返回,当找不到手头特定映射类型的条目时。 如果上面的代码有效,那么我认为我可以产生所需的行为。

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

除非类模板专用化已显式实例化或显式专用化,

否则当在需要完全定义的对象类型的上下文中引用专用化时,或者当类类型的完整性影响程序的语义时,将隐式实例化类模板专用化。[...]

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

[...]函数模板、变量模板、成员模板、

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

请注意,此列表中缺少类模板。因此,我相信,编译器理论上被允许实例化,std::map<DT, DT>甚至认为没有必要这样做。如果实例化std::mapDT作为键和值类型的模板无效,则会遇到问题。我找不到有关使用不支持比较运算符的密钥类型实例化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,因为您的标签(不确定您是否需要参数是具有两个类型参数的模板的实例)就足够了......

您已经得到了问题的答案,但是对于本文的读者来说,问题的上下文同样很有趣,因此我认为值得一提的是,您可以将标签调度用于您自己的用例,以:

  • 在编译时为特定类型(例如int)或类型组(例如 通用KVstd::map<K, V>
  • )

如果没有标记调度,这可能会很棘手,因为您可能无法部分专用化函数模板。

例如:

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

但是,这并不能解决您希望(根据您对自己帖子的评论)在运行时指定泛型类型(例如std::map<K, V>)的回退默认配置字符串的值。