当不存在 xvalue 时,移动语义如何应用于以下代码段?

How does move semantics apply on the following snippet when no xvalue is present?

我偶然发现了 following article 并且不理解 C++98 和 C++11 之间的性能差异,正如作者所说,归因于移动语义。

#include <vector>

using namespace std;

int main() {
    vector<vector<int> > V;

    for(int k = 0; k < 100000; ++k) {
        vector<int> x(1000);
        V.push_back(x);
    }

    return 0;
}

据我所知,V.push_back(x) 不调用任何移动语义。我相信 x 是一个左值,并且此代码段在 C++98 和 C++11 中调用了相同的 vector::push_back(const T&)

代码在任一版本上的编译结果相同:https://godbolt.org/z/q3Lzae

是作者的说法不正确,还是编译器足够聪明,意识到x即将被摧毁?

如果作者不正确,C++11 中是否还有其他任何东西可以提高性能 "without changing a line of code"?

你说得对,对象 x 不会被移走。获得性能的移动操作与 V.

中已有的其他 k 向量有关

随着向量的增长(除非 reserve 使用了足够大的大小),它有时需要重新分配以获得更大的内存块,因为它的元素需要在连续的内存中。这不会发生在每个 push_back 上,但在本例中有时肯定会发生。所以假设 push_back 和其他函数使用一些私有函数 grow_capacity,它获得足够的内存,然后在该内存中的向量中创建已经存在的对象。

在 C++03 中,对于任意模板参数 T,在新内存中创建对象的唯一合理方法是使用 T.[=32 的复制构造函数=]

// C++03 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
    T* new_data = get_allocator().allocate(new_capacity);
    T* new_end = new_data;
    try {
        for (const_iterator iter = begin(); iter != end(); ++iter) {
            ::new(static_cast<void*>(new_end)) T(*iter); // T copy ctor!
            ++new_end;
        }
    } catch (...) {
        while (new_end != new_data) (--new_end)->~T();
        get_allocator().deallocate(new_data, new_capacity);
        throw;
    }

    // Clean up old objects and memory.
    for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
        riter->~T();
    get_allocator().deallocate(_data, _capacity);

    // Assign private members.
    _data = new_data;
    _capacity = new_capacity;
}

在 C++11 及更高版本中,当 std::vector<T> 需要重新分配到更大的容量时,允许移动其 T 元素而不是复制它们,如果它可以这样做而不破坏强大的异常保证。这要求声明移动构造函数不抛出任何异常。但是,如果移动构造函数可能会抛出异常,则需要以旧方式复制元素,以确保在发生这种情况时向量将保持一致状态。

// C++17 implementation?
template <typename T, typename Alloc>
std::vector<T, Alloc>::grow_capacity(::std::size_t new_capacity)
{
    T* new_data = get_allocator().allocate(new_capacity);

    if constexpr (::std::is_nothrow_move_constructible_v<T>) {
        ::std::uninitialized_move(begin(), end(), new_data);   // T move ctor!
    } else {
        T* new_end = new_data;
        try {
            for (const T& old_obj : *this) {
                ::new(static_cast<void*>(new_end)) T(old_obj); // T copy ctor!
                ++new_end;
            }
        } catch (...) {
            while (new_end != new_data) (--new_end)->~T();
            get_allocator().deallocate(new_data, new_capacity);
            throw;
        }
    }

    for (const_reverse_iterator riter = rbegin(); riter != rend(); ++riter)
        riter->~T();
    get_allocator().deallocate(_data, _capacity);

    // Assign private members.
    _data = new_data;
    _capacity = new_capacity;
}

因此在类型为 std::vector<std::vector<int> > 的容器中,Tstd::vector<int>。以 C++03 的方式增加容量有时需要大量的复制构造函数,然后是 std::vector<int> 的析构函数。每个复制构造函数分配一些内存并复制 1000 int 个值,每个析构函数释放一些内存,所以这真的会加起来。但是对于 C++11 std::vector,由于元素类型 std::vector<int> 确实有一个 noexcept 移动构造函数,std::vector<std::vector<int>> 容器可以只使用那个移动构造函数,这只是一些标量成员的交换,也导致旧对象的析构函数什么也不做。

这个例子中发生的事情是 x 即将超出 push_back 调用的范围(它的生命周期结束,并且没有后续使用),所以编译器 可能 将其视为一个 xvalue 并移出它。它不是编译器需要进行移动优化的情况之一,所以它可能不会,但如果启用了优化,任何体面的编译器都会这样做(gcc 和 clang 都会在这里使用移动)。