【C++11】从std::string str = “foo”说开去


最近因为某些原因决定重新开始学习C++。考虑到自己在大学里面学到的C++有点旧(估计是C++98),所以打算从C++11开始。

C++11如其名,是2011年出来的标准,所以2011年之后才有编译器实现。现在2019年大部分PC以及服务器应该都支持C++11了。

个人习惯于看书来学习某样东西,所以找了C++相关书的资料。一开始在o’reilly上找,发现很多书都比较旧。虽然有Effective C++以及More Effective C++系列,但对于初学者来说还不是时候。最后在Stackoverflow上找到了一个比较全的推荐书列表

https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list

比如面向初学者

  • 《C++ Primer》(The fifth edition (released August 16, 2012) covers C++11)
  • 《Programming: Principles and Practice Using C++》 (Bjarne Stroustrup, 2nd Edition – May 25, 2014)

面向有编程经验的人

  • 《A Tour of C++》
  • 《Accelerated C++》

个人最后选择了可能是最全的C++作者写的《The C++ Programming Language》。原因也很简单,希望全面地,与时俱进地学习C++11。

到现在,看了100多页(总共有1300多页),不得不说,C++11和大学学的有很大不同,以及C++和C最好还是分开来看,C的编程理念和C++还是有所不同的。

具体到实际代码,我想以

std::string str = "foo";

为例子展开一下。

首先,右边的string literal,类型是const char*,和左边的std::string不同。那么string literal被转换成了string么?

直观上可以怎么理解,但背后不是这样。这里“转换”的前提是string有一个参数是const char*的构造函数。

考虑自己写一个StringLiteral类,设计一个以const char*类型为参数的构造函数。

// immutable
class StringLiteral {
public:
    // don't add explicit here
    StringLiteral(const char *chars) {
        std::cout << "StringLiteral(const char*)\n";
        length_ = std::strlen(chars);
        chars_ = chars;
    }

    // copy
    // StringLiteral(const StringLiteral &liternal) = delete;
    StringLiteral(const StringLiteral &literal) {
        std::cout << "StringLiteral(copy)\n";
        length_ = literal.length_;
        chars_ = literal.chars_;
    }

    // copy assignment not allowed
    StringLiteral &operator=(const StringLiteral &) = delete;

    // move
    // StringLiteral(StringLiteral &&) = delete;
    StringLiteral(StringLiteral && literal) { 
        std::cout << "StringLiteral(move)\n"; 
        length_ = literal.length_;
        chars_ = literal.chars_;
    };

    // move assignment not allowed
    StringLiteral &operator=(StringLiteral &&) = delete;

    size_t length() const { return length_; }

    char char_at(int i) const {
        if(i < 0 || i >= length_) {
            throw std::out_of_range{"out of range"};
        }
        return chars_[i];
    }

    friend std::ostream& operator<<(std::ostream& os, const StringLiteral& literal) {
        for(int i = 0; i < literal.length_; ++i) {
            os << literal.chars_[i];
        }
        return os;
    }

    ~StringLiteral() {
        std::cout << "~StringLiteral()\n";
    }

private:
    size_t length_;
    const char *chars_;
};

int main() {
    StringLiteral sl1 = "foo";
    std::cout << sl1 << std::endl;
    return 0;
}

执行上述程序,你可以发现输出了以下内容

StringLiteral(const char*)
foo
~StringLiteral()

也就是说,构造函数被调用了。这不是碰巧试出来的特性,而是一个叫做converting constructor的东西。简单来说,如果你构造函数上没有加explicit的话,就可以作为converting constructor。换句话说,如果上述程序中,构造函数前加了explicit(注释don’t add explicit here),那么程序就无法编译。加了explicit的话,StringLiteral必须以类似下面这种显示的方法构造

StringLiteral sl1{"foo};

回到没有explicit的程序的输出。从程序输出来看,似乎仅仅是StringLiteral的构造函数被调用了,又考虑到StringLiteral不需要被move,那么是否可以把StringLiteral的move constructor给delete掉?

答案是不可以(这里个人觉得是C++一个很不直观,或者说坑人的地方,虽然move构造函数没有被调用但是不允许删除)。move函数不能被删除的理由,个人理解是,可能部分编译器不支持elide-constructor(构造函数消除)优化。

同样是上述程序,在编译的时候,加入 -fno-elide-constructors 标志,即取消构造函数消除优化

g++ main.cpp -std=c++11 -fno-elide-constructors -o string_liternal

再次执行时,你可以看到以下输出

StringLiteral(const char*)
StringLiteral(move)
~StringLiteral()
foo
~StringLiteral()

似乎和之前的结果不一样?实际编译器做的事情类似

StringLiteral temp{"foo"};
StringLiteral stl1{std::move(temp)};

如果你高兴,你还可以注释掉move constructor,看下结果

StringLiteral(const char*)
StringLiteral(copy)
~StringLiteral()
foo
~StringLiteral()

从move constructor变成了copy constructor。实际代码类似

StringLiteral temp{"foo"};
StringLiteral sl1{temp};

这里到底发生了什么?

答案在这里

再看一次

std::string str = "foo";

在C++里这其实触发了copy-initialization。具体来说内部使用converting constructor构造了一个临时string变量,然后使用copy constructor再次构造了str。注意这里并不会调用copy assignment构造函数(上面的程序中copy/move assignment都delete掉了,间接证明了不会被调用)。

似乎没有move constructor的事情?介绍copy initialization的页面中有一句

If other is an rvalue expression, move constructor will be selected by overload resolution and called during copy-initialization. There is no such term as move-initialization.

也就是说,如果有用户定义了的move constructor的话,会优先使用move constructor。

至此,为什么输出会有copy/move构造函数都可以理解了。

当然,这里明显有一次多余的构造,所以编译器会执行构造消除优化。

小结

老实说C++很难,从上面这一个语句背后需要理解的概念就说明完全理解C++,或者说避免C++的坑需要学习很多东西。不管怎么说,一点一点积累,把基础打扎实最重要。