C++标准允许为自定义类型专门化std::to_string吗?

48

在C++11及更高版本中,是否允许在std命名空间中为自定义类型特化std::to_string

namespace std {
string to_string(::MyClass const & c) { return c.toString(); }
}

使用示例:

int main() {
    MyClass c;
    std::cout << std::to_string(c) << std::endl;
}
4个回答

37
在C++11及以上版本中,是否允许在std命名空间中为自定义类型专门化std::to_string?不可以。首先,它不是一个模板函数,因此您根本无法对其进行专门化。如果您正在询问添加自己的重载函数,则答案仍然相同。扩展命名空间std文档片段如下:添加声明或定义到namespace std或任何嵌套在std内的命名空间都是未定义行为,但以下几个例外除外。只有在声明依赖于用户定义的类型并且专业化满足原始模板的所有要求(除了禁止这些专业化的情况)时,才允许将模板专业化添加到namespace std中。

实际上,一切都很可能正常工作,但严格来说,标准并不保证会发生什么。


编辑:我没有访问官方标准的权限,因此以下内容来自免费工作草案(N4296)

17.6.4.2 命名空间使用
17.6.4.2.1 命名空间 std
1. 除非另有规定,否则 C++ 程序在向命名空间 std 或命名空间 std 中的命名空间添加声明或定义时,其行为是未定义的。程序只能在声明依赖于用户定义类型并且符合标准库模板要求且未被明确禁止的情况下,将任何标准库模板的模板特化添加到命名空间 std。
2. 如果程序声明:
- 任何标准库类模板的任何成员函数的显式特化,或 - 任何标准库类或类模板的任何成员函数模板的显式特化,或 - 任何标准库类或类模板的任何成员类模板的显式或部分特化,
则其行为是未定义的。程序只能显式实例化在标准库中定义的模板,而且该实例化必须依赖于用户定义类型的名称,并且满足原始模板的标准库要求。
3. 翻译单元不得将命名空间 std 声明为内联命名空间 (7.3.1)。

3
没有保证......未定义?我无法相信这是未定义的行为。为任何其他命名空间进行专门化都能正常工作。我非常怀疑在程序行为方面会有任何后果。我认为标准应该只是说它被禁止,如果它确实不是未定义的话就不要称它为未定义。这怎么可能导致未定义的行为? - Brandon
10
我们一般希望编译器在我们进行禁止操作时发出错误警告,但是要求编译器检测并诊断此类问题是不现实的。因此,会出现未定义行为(UB)。 - Lightness Races in Orbit
1
这真是令人沮丧。我希望他们能为std::to_string做个例外。或者作为替代方案,提供其他的特化方式,比如使用类似于template <class T> auto f(T && t) -> std::enable_if_t<ToStringSpec<std::decay_t<T> >::value, void> { ToStringSpec<std::decay_t<T> >()(std::forward<T>(t)); }的方法。 - jotik
2
17.6.4.2引用中所提到的脚注:*181) 任何实例化其他库模板的库代码都必须准备好与符合标准最低要求的任何用户提供的专业化一起正常工作。 - Wolf
写一个类型转换运算符 operator std::string 作为 to_string 的替代方案怎么样? - jaques-sam
1
@jaques-sam 这太丑了。必须有一致的方法将东西转换为字符串。这样,当您编写通用模板代码时,就不必解决使用std::to_stringstatic_cast<std::string>的情况。前者更美观。标准库如此不一致真是可惜。有些情况提供了自定义点,而有些情况则像std::to_string一样。 - Sergey Kolesnik

19

如果我没记错的话,您可以简单地为通用类型重载to_string

template<typename T> to_string(const T& _x) {
    return _x.toString();
}

这使得您的程序可以使用ADL(参数相关查找)来根据传递的类型正确选择相关的to_string方法。


3
您是在建议将此操作放置于 std 命名空间内还是外部? - James Adkison
8
@JamesAdkison 外面的部分。 - sjrowlinson
3
这只适用于未经资格认证的函数调用,例如 to_string(myObject),但不适用于已经经过资格认证的函数调用,例如 std::to_string(myObject) - jotik
7
如果这样写,那它就是不正确的。正确的方式是使用 using std::to_string; to_string(myObject); 这样做可以无论 myObject 的类型是什么都能得到正确的结果。 - spectras
1
@spectras +1。我也希望这个答案有更多的投票,以便回答下一个合乎逻辑的问题:“你如何提供自己的 to_string 函数?” - iwat0qs
显示剩余3条评论

14
在C++11及以上版本中,是否允许在std命名空间中专门为自定义类型定制std::to_string?不可以,在std命名空间中添加to_string()的重载。好消息是,您不需要这样做,有一个简单的解决方案!您可以提供自己的实现,并让ADL(参数依赖查找)为您解决问题。方法如下:
class A {};

std::string to_string(const A&)
{
    return "A()";
}

int main()
{
    A a;
    using std::to_string;
    std::cout << to_string(2) << ' ' << to_string(a);
}

这里使用了using声明来引入std::to_string,然后使用未限定调用的方式调用to_string()
现在,std::to_string::to_string都可见,编译器会选择适当的重载函数。
如果你不想每次在使用to_string之前写using std::to_string,或者担心忘记使用不带命名空间的to_string,你可以创建一个辅助函数。
template<typename T>
std::string my_to_string(T&& t)
{
    using std::to_string;
    return to_string(std::forward<T>(t));
}

请注意,该函数可以在任何命名空间中定义,并且独立于定义类的命名空间工作(它们不必相同)。
请参见示例
注意:如果您是调用to_string的人,则此方法有效。如果有一个库调用std::to_string并且您想为自己的类型更改它,则无法实现。

如果您需要在调用std::to_string时使库调用您的函数,我认为链接器替换可能会有所帮助。在TDD中使用时,当您依赖于无法编辑但仍必须在测试中模拟的库时。在这种情况下,您必须从替换后的库将所有调用转发到默认的std库,除了std::to_string,它将调用您的实现,然后再调用您的to_string或实际的std::to_string - KulaGGin

10
更好的方法是创建自己的函数,如果可能的话使用std::to_string,并在传递参数时尽可能使用.toString()方法:
#include <type_traits>
#include <iostream>
#include <string>

struct MyClass {
   std::string toString() const { return "MyClass"; }
};

template<class T>
typename std::enable_if<std::is_same<decltype(std::declval<const T&>().toString()), std::string>::value, std::string>::type my_to_string(const T &t) {
    return t.toString();
}

template<class T>
typename std::enable_if<std::is_same<decltype(std::to_string(std::declval<T&>())), std::string>::value, std::string>::type my_to_string(const T &t) {
    return std::to_string(t);
}

int main() {
   std::cout << my_to_string(MyClass()) << std::endl; // will invoke .toString
   std::cout << my_to_string(1) << std::endl; //will invoke std::to_string
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接