【问题标题】:is it possible to implement a std::move-and-clear function?是否可以实现 std::move-and-clear 功能?
【发布时间】:2016-10-27 00:02:16
【问题描述】:

是否可以编写一个函数 move_and_clear 这样, 对于任何 STL 容器:

do_something_with(move_and_clear(container));

相当于:

do_something_with(std::move(container));
container.clear();

?

这是我的第一次尝试,但不起作用。 我想我的类型是正确的 (虽然这个的生产版本可能会洒在一些 std::remove_reference's),它编译成功, 但它失败或崩溃,因为在它超出范围后访问了临时。

template<class T>
T &&move_and_clear(T &t)
{
    T scratch;
    std::swap(scratch, t);
    return std::move(scratch);
}

这是我的第二次尝试。这确实有效,但它是一个预处理器宏,因此是邪恶的:

template<class T>
T &&move_and_clear_helper(T &t, T &&scratch)
{
    std::swap(scratch, t);
    return std::move(scratch);
}
#define move_and_clear(t) move_and_clear_helper(t, decltype(t)())

我的第三次尝试是另一个同样有效的宏,这次 使用 lambda 而不是命名的辅助函数。 所以它比之前的宏更独立一些, 但也许可读性较差,当然它仍然是邪恶的,因为它是一个宏:

#define move_and_clear(t) \
    [](decltype(t) &tparam, decltype(t) &&scratch){ \
        std::swap(scratch, tparam); \
        return std::move(scratch); \
    }(t, decltype(t)())

这是一个包含我的三个尝试的可编译程序:

/*
    g++ --std=c++11 -W -Wall -g move_and_clear.cc -o move_and_clear1 -DWHICH=1
    g++ --std=c++11 -W -Wall -g move_and_clear.cc -o move_and_clear2 -DWHICH=2
    g++ --std=c++11 -W -Wall -g move_and_clear.cc -o move_and_clear3 -DWHICH=3
    ./move_and_clear1   # assert-fails
    ./move_and_clear2   # succeeds
    ./move_and_clear3   # succeeds
*/

#include <assert.h>
#include <iostream>
#include <memory>
#include <vector>

#if WHICH == 1
    template<class T>
    T &&move_and_clear(T &t)
    {
        T scratch;
        std::swap(scratch, t);
        return std::move(scratch);
    }
#elif WHICH == 2
    template<class T>
    T &&move_and_clear_helper(T &t, T &&scratch)
    {
        std::swap(scratch, t);
        return std::move(scratch);
    }
    #define move_and_clear(t) move_and_clear_helper(t, decltype(t)())
#elif WHICH == 3
    #define move_and_clear(t) \
        [](decltype(t) &tparam, decltype(t) &&scratch){ \
            std::swap(scratch, tparam); \
            return std::move(scratch); \
        }(t, decltype(t)())
#endif

// Example "do_something_with":
// takes an rvalue reference to a vector that must have size 3,
// steals its contents, and leaves it in a valid but unspecified state.
// (Implementation detail: leaves it with 7 elements.)
template<typename T>
void plunder3_and_leave_in_unspecified_state(std::vector<T> &&v)
{
  assert(v.size() == 3);
  std::vector<T> pirate(7);
  assert(pirate.size() == 7);
  std::swap(pirate, v);
  assert(pirate.size() == 3);
  assert(v.size() == 7);
}

int main(int, char**)
{
    {
        std::cout << "Using std::move and clear ..." << std::endl << std::flush;
        std::vector<std::unique_ptr<int>> v(3);
        assert(v.size() == 3);
        plunder3_and_leave_in_unspecified_state(std::move(v));
        assert(v.size() == 7); // (uses knowledge of plunder's impl detail)
        v.clear();
        assert(v.empty());
        std::cout << "done." << std::endl << std::flush;
    }
    {
        std::cout << "Using move_and_clear ..." << std::endl << std::flush;
        std::vector<std::unique_ptr<int>> v(3);
        assert(v.size() == 3);
        plunder3_and_leave_in_unspecified_state(move_and_clear(v));
        assert(v.empty());
        std::cout << "done." << std::endl << std::flush;
    }
}

有没有办法在不使用宏的情况下将 move_and_clear 实现为模板函数?

【问题讨论】:

  • “清晰”是什么意思? -除非,你还想清除容器使用的底层内存分配,为什么不简单地使用clear()成员函数呢? - 顺便说一句,std::move 已经窃取了内容
  • @WhiZTiM 在容器的 clear() 成员函数的意义上“清除”,正如我在开头对问题的明确描述中所说的那样。因为,例如,我有在一百个不同的地方调用 std::move 后跟 clear() 的代码,而且很容易忘记或放错 clear(),所以我想将它们组合成一个调用,如果可能。
  • do_something_with()调用clear()不是更好吗?
  • @el.pescado 也许,但我没有写 do_something_with() 所以这不是一个选项。例如,说 do_something_with() 是一个移动构造函数,它使源处于“有效但未指定的状态”。
  • @DonHatch 在标准容器中,只有 std::string 在移出时可能不清楚(当 SBO 发生时),即使在那里,我怀疑每个实现都会做一点工作来设置size 为 0。在实践中,实际移动(不是调用std::move,而是移动并消耗结果)将清除。你只是偏执,还是有一个实际的例子,std::move 没有做你要求发生的事情?

标签: c++ c++11 move-semantics rvalue-reference


【解决方案1】:

是否可以编写一个函数 move_and_clear 使得对于任何 STL 容器:

do_something_with(move_and_clear(container));

相当于:

do_something_with(std::move(container));
container.clear();

?

template<typename T>
T move_and_clear(T& data){
     T rtn(std::move(data));
     data.clear();
     return rtn;
}

返回值将在调用站点被视为rvalue

同样,这将享受 返回值优化(在任何理智的编译器中)的好处。最肯定的是,内联。请参阅Howard Hinnant's answer to this 问题。


同样,STL 容器具有移动构造函数,但对于任何其他自定义容器,最好将其限制为 move-constructible 类型。否则,您可以使用不会移动的容器调用它,并且那里有必要的副本。

template<typename T>
auto move_and_clear(T& data)
-> std::enable_if_t<std::is_move_constructible<T>::value, T>
{
     T rtn(std::move(data));
     data.clear();
     return rtn;
}

见:This answer

编辑:

如果您担心 RVO,我不知道任何主要的编译器不会在优化构建中执行 RVO(除非通过开关显式关闭)。还有一个proposal to make it mandatory,希望我们应该在 C++17 中看到它。

EDIT2:

论文进入C++17的工作草案,见this

【讨论】:

  • 参数不应该是T&amp;&amp; data吗?
  • 不,这是个坏主意:使用 Forwarding Reference T&amp;&amp; 将允许该函数甚至绑定到临时对象和 const 对象,我不是确保这很有意义,因为您无法从 const
  • @el.pescado,不,它不会,返回值优化肯定会在(在任何理智的编译器中)中发挥作用。在您的 return 语句中使用 std::move 实际上可能会导致性能悲观或更糟的是导致错误。请参阅Howard Hinnant's answer to this 问题。
  • @DonHatch,见Howard Hinnant's answer to this question。以上是执行此操作的最简单有效的方法。虽然,该标准并没有强制 RVO 启动。我不知道 任何主要的 编译器不会在优化构建中执行 RVO(除非通过开关显式关闭)。有一个proposal to make it mandatory,希望我们应该在 C++17 中看到它。
  • @DonHatch 是的,这几乎可以保证。在为本地x 执行return x; 时,标准强制编译器首先将x 视为右值(即从它移出)。仅当失败时才考虑复制。
【解决方案2】:

这是一个不需要额外移动容器的实现,但引入了一个代理对象:

template <class T>
class ClearAfterMove
{
  T &&object;

public:
  ClearAfterMove(T &&object) : object(std::move(object)) {}

  ClearAfterMove(ClearAfterMove&&) = delete;

  ~ClearAfterMove() { object.clear(); }

  operator T&& () const { return std::move(object); }
};


template <class T>
ClearAfterMove<T> move_and_clear(T &t)
{
  return { std::move(t) };
}

它的工作原理是它创建了一个不可移动对象ClearAfterMove,它将包装源容器t,并在它(包装器)超出范围时在t 上调用clear

在这样的电话中:

do_something_with(move_and_clear(container));

一个临时的ClearAfterMove 对象(我们称之为cam)包装container 将通过调用move_and_clear 创建。然后这个临时值将被其转换运算符转换为右值引用并传递给do_something_with

临时变量在创建它们的完整表达式结束时超出范围。对于cam,这意味着一旦解决了对do_something_with 的调用,它将被销毁,这正是您想要的。

请注意,这样做的优点是不会产生任何额外的移动,而所有代理对象解决方案都有一个缺点:它不能很好地使用类型推导,例如:

auto x = move_and_clear(y);

【讨论】:

  • +1 不错!这比@WhiZTiM 的回答更符合我最初的尝试,而且这并没有像他的回答那样弯曲我的大脑。但是他更短,我认为它们在优化后都是等价的......所以我会尽量习惯他的方式。
【解决方案3】:
template<class T>
T &&move_and_clear(T &t)
{
  T scratch;
  std::swap(scratch, t);
  return std::move(scratch);
}

这很接近。但是你输入了很多:

template<class T>
T move_and_clear(T &t)
{
  T scratch;
  std::swap(scratch, t);
  return scratch;
}

scratch 将与返回值一起被省略(除非有人设置恶意编译器标志)。

略有改进:

template<class T>
T move_and_clear(T &t)
{
  T scratch;
  using std::swap;
  swap(scratch, t);
  return scratch;
}

启用 Koenig 查找 swap,这意味着如果有人在他们的容器上编写了一个比 std::swap 更有效的免费 swap 操作,它将被调用。


现在,在实践中,std 容器的成功移动将清除它。当 SSO(小字符串优化)处于活动状态时,唯一不清除源容器甚至有点意义的情况是 std::basic_string,并且清除所需的工作量非常小,我看不到库编写者不这样做为了理智起见。

但是,带有交换的这段代码保证源对象将具有T 类型的空对象的状态。

请注意,这适用于所有可能的 C++ 实现:

template<class T>
T move_and_clear(T &t)
{
  T scratch = std::move(t);
  return scratch;
}

对于大多数std容器,对move-construction的要求比较严格,迭代器必须要迁移。迭代器转移意味着在实践中必须清源。

然而,在std::basic_string 上,由于小字符串优化,迭代器不必传输。我知道不能保证源 basic_string 将被清除(但我很容易出错:标准中可能有一个条款明确要求:它只是不被操作的其他语义所暗示)。

【讨论】:

  • 我很难弄清楚你的位置;也许稍微改写一下以进行澄清会有所帮助。一方面,您似乎主张依靠移动明确的假设(可能除了 std::basic_string?),尤其是在您对原始问题的第一条评论中。但是你似乎在说相反的意思,即它不能被依赖(“请注意,这 not 工作 [...]”)。也许只是添加“但这不能依赖”。到以“现在在实践中”开头的第一段的结尾(如果你打算这么说的话。)
  • 你做了一个有趣的陈述“迭代器转移意味着它必须在实践中清除源”。你能多说一下为什么这个含义成立吗?从表面上看,我不明白为什么..虽然我没有深入思考过。
【解决方案4】:

std::move 被定义为移动内容,因此源容器将被有效地清除。

(或者我可能误解了你的问题)

【讨论】:

  • 顺便说一句,移动的容器不需要是空的。
  • 不,std::move 不移动任何东西;见en.cppreference.com/w/cpp/utility/move。根据该参考资料,“除非另有说明,否则所有已从中移动的标准库对象都处于有效但未指定的状态。”所以我的目标是写一些确实表现如你所描述的东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-03-24
  • 2017-08-21
  • 1970-01-01
  • 2014-10-14
  • 2012-12-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多