是否可以实现std :: move-and-clear功能?

时间:2016-06-24 11:19:36

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

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

do_something_with(move_and_clear(container));

相当于:

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

这是我的第一次尝试,但不起作用。 我想我的类型是正确的 (虽然这个的生产版本可能会有所体现 std :: remove_reference()),它成功编译, 但它失败或崩溃,因为刮擦超出范围后会被访问。

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实现为模板函数,而不使用宏?

4 个答案:

答案 0 :(得分:6)

  

是否可以编写一个函数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

答案 1 :(得分:1)

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

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,并在它clear时调用t包装器)超出范围。

在这样的电话中:

do_something_with(move_and_clear(container));

ClearAfterMove的调用将创建包裹cam的临时container对象(让我们称之为move_and_clear)。然后,此临时值将由其转换运算符转换为右值引用,并传递给do_something_with

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

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

auto x = move_and_clear(y);

答案 2 :(得分:0)

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

(或者我可能会错误地回答你的问题)

答案 3 :(得分:0)

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;
}

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

现在,在实践中,成功移动std容器将清除它。当SSO(小字符串优化)处于活动状态时,唯一没有清除源容器的地方就是std::basic_string,并且执行清除所需的工作量非常小我无法看到库编写器不是为了理智而这样做。

但是,带有swap的代码保证源对象具有类型为T的空对象的状态。

请注意,在所有可能的C ++实现中都有效:

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

对于大多数std容器,移动构造的要求更严格,迭代器必须转移。迭代器转移意味着它必须在实践中清除源。

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