【问题标题】:How to avoid a common bug in a large codebase?如何避免大型代码库中的常见错误?
【发布时间】:2009-01-05 06:44:36
【问题描述】:

有没有办法取消为 chars 和 wchar_t 定义字符串和 wstrings 上的 +=?

基本上我想避免以下错误:

int age = 27;

std::wstring str = std::wstring(L"User's age is: ");
str += age;

std::string str2 = std::string("User's age is: ");
str2 += age;

上面的代码会将ASCII字符27添加到字符串中,而不是数字27。

我显然知道如何解决这个问题,但我的问题是:在这种情况下如何产生编译器错误?

注意:您可以覆盖 std::string 和 int 上的 += 以正确格式化字符串,但这不是我想要做的。我想完全禁止在这些操作数上使用此运算符。

【问题讨论】:

  • 我会说这就是我所说的非问题。你总能找到方法让自己在脚下开枪。更好地教你的同事正确编程 C++ :)
  • 射中自己的脚是不可避免的。因此,您必须编写代码以确保不会在自己的脚下开枪。否则你的代码很糟糕。
  • 它们没有冲突。第一个声明是评论 litb 的回应。第二个陈述是说,如果你有办法搞砸,超过数百万行代码,你最终会做到的。所以编码更好,并通过例如生成编译错误使其成为不可能。
  • 例如:您可以将所有指针作为 void* 传递,然后在您进入函数后将它们 C 转换回原始类型。这是一个你不应该做的事情的例子,因为最终你会射中自己的脚。
  • “只要你从错误中吸取教训。” - 是的,如果你很聪明,你最终会学会不让这种情况成为可能。

标签: c++ string


【解决方案1】:

您不能停用类(此处为 std::basic_string)的特定功能,因为它的接口清楚地(并且正式地)允许该操作。试图重载操作符只会把事情搞砸。

现在,你可以在另一个类中“包装”std::basic_string,使用私有继承或组合,然后使用公共接口作为 std::basic_string 部分的代理,但只有您希望使用的功能。

我建议先用 typedefs 替换你的字符串类型:

namespace myapp
{
    typedef std::string String;
    typedef std::wstring UTFString;
}

然后,一旦您的应用程序在将 std::string 和 std::wstring 替换为 myapp::String 和 myapp::UTFString(这些是示例名称)后编译正常,您就可以在某处定义包装类:

namespace myapp
{
/** std::basic_string with limited and controlled interface.
    */
    template< class _Elem, class _Traits, class _Ax >
    class limited_string 
    {
    public:
        typedef std::basic_string< _Elem , _Traits, _Ax > _String; // this is for easier writing
        typedef limited_string< _Elem, _Traits, _Ax > _MyType; // this is for easier writing

    private:
        _String m_string; // here the real std::basic_string object that will do all the real work!

    public:

        // constructor proxies... (note that those ones are not complete, it should be exactly the same as the original std::basic_string
        // see some STL docs to get the real interface to rewrite)
        limited_string() : m_string {}
        limited_string( const _MyType& l_string ) : m_string( l_string.m_string ) {}
        limited_string( const _Elem* raw_string ) : m_string( raw_string ) {}
        //... etc...

        // operator proxies...
        _MyType& operator= ( const _MyType& l_string ) 
        {
            m_string = l_string.m_string;
        }
        // etc...
        // but we don't want the operator += with int values so we DON'T WRITE IT!

        // other function proxies...
        size_t size() const { return m_string.size(); } // simply forward the call to the real string!
        // etc...you know what i mean...

        // to work automatically with other STL algorithm and functions we add automatic conversion functions:
        operator const _Elem*() const { return m_string.c_str(); } 

        // etc..        


    };
}

...然后,您只需替换这些行:

// instead of those lines...
    //typedef std::string String; 
    //typedef std::wstring UTFString;

    // use those ones
    typedef limited_string< char, std::char_traits<char>, std::allocator<char> >                String; // like std::string typedef
    typedef limited_string< wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >       UTFString; // like std::wstring typedef 

...你的例子会崩溃:

error C2676: binary '+=' : 'myapp::UTFString' does not define this operator or a conversion to a type acceptable to the predefined operator
error C2676: binary '+=' : 'myapp::String' does not define this operator or a conversion to a type acceptable to the predefined operator

这是我为证明这一点而编写的完整测试应用程序代码(在 vc9 上编译):

#include <string>
#include <iostream>

namespace myapp
{

    /** std::basic_string with limited and controlled interface.
    */
    template< class _Elem, class _Traits, class _Ax >
    class limited_string 
    {
    public:
        typedef std::basic_string< _Elem , _Traits, _Ax > _String; // this is for easier writing
        typedef limited_string< _Elem, _Traits, _Ax > _MyType; // this is for easier writing

    private:
        _String m_string; // here the real std::basic_string object that will do all the real work!

    public:

        // constructor proxies... (note that those ones are not complete, it should be exactly the same as the original std::basic_string
        // see some STL docs to get the real interface to rewrite)
        limited_string() : m_string {}
        limited_string( const _MyType& l_string ) : m_string( l_string.m_string ) {}
        limited_string( const _Elem* raw_string ) : m_string( raw_string ) {}
        //... etc...

        // operator proxies...
        _MyType& operator= ( const _MyType& l_string ) 
        {
            m_string = l_string.m_string;
        }
        // etc...
        // but we don't want the operator += with int values so we DON'T WRITE IT!

        // other function proxies...
        size_t size() const { return m_string.size(); } // simply forward the call to the real string!
        // etc...you know what i mean...

        // to work automatically with other STL algorithm and functions we add automatic conversion functions:
        operator const _Elem*() const { return m_string.c_str(); } 

        // etc..        


    };

    // instead of those lines...
    //typedef std::string String; 
    //typedef std::wstring UTFString;

    // use those ones
    typedef limited_string< char, std::char_traits<char>, std::allocator<char> >                String; // like std::string typedef
    typedef limited_string< wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >       UTFString; // like std::wstring typedef 
}

int main()
{
    using namespace myapp;

    int age = 27;

    UTFString str = UTFString(L"User's age is: ");
    str += age; // compilation error!
    std::wcout << str << std::endl;

    String str2 = String("User's age is: ");
    str2 += age; // compilation error!
    std::cout << str2 << std::endl;

    std::cin.ignore();

    return 0;
}

我认为它会彻底解决您的问题,但您必须包装所有功能。

【讨论】:

  • 没问题 :) 我想确定它是可行的,所以我先尝试了。一种防御性编程心态:p
  • 为什么不从basic_string派生private,并使用using声明在你的类范围内重新声明basic_string的函数?我想我更喜欢这种方法
  • 是的,我建议在第二段中,我没有检查,但我想应该是一样的。
  • @litb:您不应该将字符串类用作派生其他类型的基类。这通过没有虚拟析构函数来表示。
  • 马丁,是的,因为我会派生私人。你不能从外面调用删除它。但是,实际上,我刚刚阅读了herb Sutter 的一篇文章,坦率地说,我不会再使用私有继承了:D
【解决方案2】:

大多数源代码控制系统都允许您在签入期间对代码运行健全性检查。因此,您可以设置一个测试来执行验证并在失败时拒绝签入:

例子:

测试脚本:

#!/bin/tcsh
# Pass the file to test as the first argument.

echo "#include <string>\
void operator+=(std::string const& , int const&);\
void operator+=(std::string const& , int);"\
| cat - $1 \
| g++ -c -x c++ -  >& /dev/null


echo $status

这个脚本伪造了上面两个运算符的添加(实际上没有改变源)。即使原始代码编译,这也会导致任何使用带有字符串和字符的 operator+ 的操作失败。

注意:operator+= 从 litb 窃取的想法。谁已经删除了他的例子。但是应该归功于它。

【讨论】:

  • 我认为这种思路是最好的解决方案。
【解决方案3】:

没有简单的方法可以防止它,但有一种简单的方法可以找到它。编写一个使用此运算符的小程序,然后查看要禁止的运算符+= 的损坏符号。这个符号是一个唯一的字符串。作为自动化测试的一部分,请使用 DUMPBIN(或等效的 Linux/Mac 工具)检查此损坏符号是否存在于您的目标文件中。

【讨论】:

    【解决方案4】:

    1) 创建一个您自己的字符串类,该类继承/包含std::string

    2) 在此类中重载 operator+=(int val) 并将其设为私有。

    3) 将此类用于所有字符串操作。

    每当您执行以下操作时,这都会使编译器标记错误:

    MyString str;
    str += 27;
    

    【讨论】:

    • 很难对整个团队强制执行,而且代码库真的很大
    • 我认为这是解决 OP 问题的好方法。但它仍然会让其他开发人员感到困惑。
    • 我认为这是唯一安全的方法
    • 我也这么认为。是时候写下我的答案了,你已经用三句话说了:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-25
    • 1970-01-01
    • 1970-01-01
    • 2021-04-11
    • 1970-01-01
    相关资源
    最近更新 更多