如何使用C++11移动语义将向量内容追加到另一个向量中?

37

请考虑以下代码片段:

class X;

void MoveAppend(vector<X>& src, vector<X>& dst) {
   dst.reserve(dst.size() + src.size());
   for (const X& x : src) dst.push_back(x);
   src.clear();
}

如果我们假设class X实现了移动语义,那么我该如何高效地实现MoveAppend

3个回答

56

只需做:

#include <iterator>
#include <algorithm>

// ...

void MoveAppend(std::vector<X>& src, std::vector<X>& dst) 
{
    if (dst.empty())
    {
        dst = std::move(src);
    }
    else
    {
        dst.reserve(dst.size() + src.size());
        std::move(std::begin(src), std::end(src), std::back_inserter(dst));
        src.clear();
    }
}
如果目标数组 dst 为空,从源数组 src 移动元素到 dst 可以达到效果——这样做的成本非常低廉,只需"窃取"被 src 封装的数组,使得 dst 指向它。
如果目标数组 dst 不为空,则向 dst 添加元素时将使用源数组 src 中的元素进行移动构造。调用std::move()后,src 将不再是空的——它将包含被移动元素的 "僵尸"。这就是为什么仍然需要调用 clear() 的原因。

1
@ŁukaszLew:它保证不会为空吗?如果它可能为空,最好先检查一下。然后您仍然可以简单地移动整个容器,这可能会更便宜。 - Benjamin Lindley
1
你能解释一下为什么这比 dst.insert(end(dst), begin(src), end(src)) 更好吗? - TemplateRex
2
@TemplateRex:这将从src的相应元素复制构造dst的新元素。OP想要的是移动。 - Andy Prowl
1
可以使用“插入(insert)”和移动迭代器(move iterators)协同工作。 - Luc Danton
5
@TemplateRex dst.insert(end(dst), make_move_iterator(begin(src)), make_move_iterator(end(src))); 可以翻译为:将 src 中的元素移动到 dst 的末尾,代码如下: - v.oddou
显示剩余4条评论

23

我稍微更喜欢这个答案,胜过被接受的答案:

#include <vector>
#include <iterator>
#include <utility>

template <typename T>
typename std::vector<T>::iterator append(const std::vector<T>& src, std::vector<T>& dest)
{
    typename std::vector<T>::iterator result;

    if (dest.empty()) {
        dest = src;
        result = std::begin(dest);
    } else {
        result = dest.insert(std::end(dest), std::cbegin(src), std::cend(src));
    }

    return result;
}

template <typename T>
typename std::vector<T>::iterator append(std::vector<T>&& src, std::vector<T>& dest)
{
    typename std::vector<T>::iterator result;

    if (dest.empty()) {
        dest = std::move(src);
        result = std::begin(dest);
    } else {
        result = dest.insert(std::end(dest),
                             std::make_move_iterator(std::begin(src)),
                             std::make_move_iterator(std::end(src)));
    }

    src.clear();
    src.shrink_to_fit();

    return result;
}

示例:

#include <string>
#include <algorithm>
#include <iostream>

int main()
{
    const std::vector<std::string> v1 {"world", "!"};

    std::vector<std::string> v2 {" "}, v3 {"hello"}, v4 {};

    append(v1, v2); // copies
    append(std::move(v2), v3); // moves
    append(std::move(v3), v4); // moves

    std::copy(std::cbegin(v4), std::cend(v4), std::ostream_iterator<std::string> {std::cout});
    std::cout << std::endl;
}

我认为这是一个更好的答案,具有左值和右值源的重载,并使用std::vector::insert()。 - dats
2
为什么要“清理”rvalue源?我认为这样做会做一些不必要的额外工作。 - dats
@dats 这是一个有趣的观点 - 我认为调用者期望在调用后rvalue源的容量为零,否则情况就不同了。 - Daniel
5
@Daniel 调用方不应期望移动对象状态的任何信息。移动对象被视为无效,不应以任何方式使用。 - grisevg
2
这个函数不应该有两个版本。源应该通过值传递,而不是通过const引用或rvalue传递。如果您通过值传递源,则调用者可以决定移动还是复制。 - mfnx
显示剩余3条评论

6

尝试略微改进@Daniel的答案:函数不应被定义两次,源应按值传递。

// std::vector<T>&& src - src MUST be an rvalue reference
// std::vector<T> src - src MUST NOT, but MAY be an rvalue reference
template <typename T>
inline void append(std::vector<T> source, std::vector<T>& destination)
{
    if (destination.empty())
        destination = std::move(source);
    else
        destination.insert(std::end(destination),
                   std::make_move_iterator(std::begin(source)),
                   std::make_move_iterator(std::end(source)));
}

现在,呼叫者可以决定是复制还是移动。
std::vector<int> source {1,2,3,4,5};
std::vector<int> destination {0};
auto v1 = append<int>(source,destination); // copied once
auto v2 = append<int>(std::move(source),destination); // copied 0 times!!

除非必须使用(例如:std::ifstream&&),否则不要将 && 用于参数。


1
“Destiny” 是一个有趣的名字选择! - Lightness Races in Orbit
2
在这种情况下,有两个向量内存分配:一个用于复制source,另一个用于扩展destination的大小。如果我们在const&&&上专门化append函数(如Daniel的答案中所示),那么我们只需要进行一次向量内存分配,以扩展destination的大小。 - William
2
如果调用者不移动(append(src, dst)),按值传递将保证两个向量分配:一个用于从调用者复制到我们的按值参数,另一个用于扩展目标向量的大小(然后通过从“源”向量移动来填充其内容)。如果调用者移动(append(move(src), dst)),则只有一个分配:扩展目标的大小。 - William
4
我想说的是,如果调用者想要复制,那么采用按值传递向量的方式意味着需要进行两次向量分配,而采用按const&传递向量的方式只需要进行一次向量分配。希望现在清楚了 :) - William
1
我投票支持这个答案,因为它很简洁,并回答了有关移动向量内容的原始问题。但是@William也有一点,即“复制”场景会在堆栈上分配一个临时向量(其元素随后由函数移动),而通过引用传递函数可以直接从源中复制元素。 因此,当复制到非空目标时,这将为该临时向量(而不是元素,只是容器)使用额外的分配。 - Michael Krebs
显示剩余7条评论

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