防止接受0的const std::string&函数

105

一图胜千言:

#include<string>
#include<iostream>

class SayWhat {
    public:
    SayWhat& operator[](const std::string& s) {
        std::cout << s << "\n";
        return *this;
    }
};

int main() {
    SayWhat ohNo;
    // ohNo[1]; // Does not compile. Logic prevails.
    ohNo[0]; // you didn't! this compiles.
    return 0;
}

编译器在将数字0传递给接受字符串的方括号运算符时不会发出警告。相反,这会编译,并在进入该方法之前失败,显示以下信息:
terminate called after throwing an instance of 'std::logic_error'
  what():  basic_string::_S_construct null not valid

供参考:

> g++ -std=c++17 -O3 -Wall -Werror -pedantic test.cpp -o test && ./test
> g++ --version
gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC)

我的猜测

编译器在进入该方法时隐式使用了std::string(0)构造函数,这没有任何好处,却导致了相同的问题(搜索上面的错误信息)。

问题

有没有办法在类的一侧修复这个问题,使API用户不会感到这个问题,并且可以在编译时检测到错误?

也就是说,添加一个重载函数。

void operator[](size_t t) {
    throw std::runtime_error("don't");
}

不是一个好的解决方案。

2
编译后的代码,在Visual Studio中以异常方式抛出,位于ohNo [0]处,异常为“0xC0000005: 访问地址0x00000000时发生访问冲突”。 - TruthSeeker
7
声明一个私有重载的operator[](),接受一个整型参数,并且不定义它。 - Peter
2
@Peter 请注意这是一个链接器错误,但这比我之前遇到的情况要好。 - kabanus
7
在上述情况下,如果操作符是私有的,将会出现编译器错误,而不是链接器错误,只有在类内部调用时才会出现链接器错误。 - Aconcagua
7
@Peter,这在没有可用C++11的情况下尤其有趣 - 即使今天仍然存在这种情况(实际上我参与的某个项目就是如此,并且我非常想要一些新功能...)。 - Aconcagua
显示剩余4条评论
3个回答

168

std::string(nullptr)也是有效的,因为nullptr是一个空指针常量。所以0匹配了接受指针的字符串构造函数,但代码违反了不能将空指针传递给std::string的前提条件。

只有字面值的0会被解释为一个空指针常量,如果它是int中的运行时值,你就不会遇到这个问题(因为此时重载解析将寻找int转换)。字面值1也不是问题,因为1不是空指针常量。

既然这是一个编译时的问题(无效的字面值),你可以在编译时捕获它。添加以下形式的重载:

void operator[](std::nullptr_t) = delete;

std::nullptr_tnullptr的类型。它将匹配任何空指针常量,无论是00ULL还是nullptr。由于该函数被删除,因此在重载分辨率期间会导致编译时错误。


这绝对是最好的解决方案,我完全忘记了我可以重载一个空指针。 - kabanus
在Visual Studio中,即使是"ohNo[0]"也会抛出空值异常。这是否意味着std::string类的实现特定? - TruthSeeker
@pmp 如果抛出异常(如果有的话),则具体实现是各不相同的,但关键点是在所有实现中字符串都是空指针。通过这种解决方案,您将无法进入异常部分,因为它会在编译时检测到。 - kabanus
18
将空指针传递给 std::string 的构造函数在 C++ 标准中是不允许的。这是未定义的行为,因此 MSVC 可以任意处理(例如抛出异常)。 - StoryTeller - Unslander Monica

29
一种选择是声明一个接受整数参数的private重载operator[](),并且不定义它。
与像void operator[](std::nullptr_t) = delete这样的选项不同,这个选项适用于所有C++标准(1998及以上)。
operator[]()设置为private成员将在示例代码中调用ohNo[0]时导致可诊断错误,除非该表达式由类的成员函数或friend使用。
如果该表达式由类的成员函数或friend使用,则代码将编译,但由于函数未定义,通常会构建失败(例如由于未定义函数而导致的链接器错误)。

3

使用string_view有所帮助

从C++17开始,我们拥有了std::string_view。它的目的正是为了传递对于字符串对象的非所有权引用,以供仅读取字符串的函数使用。你应该认真考虑在这种情况下使用它。

现在,std:: string_view也有自己的问题(参见:足够让我们陷入困境的string_view),但是在这里它会给你一个有用的警告。如果你将以下内容替换:

    SayWhat& operator[](const std::string& s) {

使用

    SayWhat& operator[](std::string_view s) {

如果你使用--std=c++17 -Wall编译,你会得到以下结果:

<source>: In function 'int main()':
<source>:16:11: warning: null argument where non-null required (argument 2) [-Wnonnull]
   16 |     ohNo[0]; // you didn't! this compiles.
      |           ^

我认为在只读情况下,比如我的模拟示例中甚至包括任何const string&函数参数,这将是更可取的解决方案。我认为在这些情况下,大多数“悬空”问题都将无关紧要(除非有人��意破坏)。 - kabanus

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