我可以在 C++ 中移动临时对象的属性吗?

Can I move attributes of temporary objects in C++?

我正在编写一个依赖于向量中的项目的迭代器和一个生成所述迭代器的迭代器工厂。该代码在概念上等同于以下内容:

struct Iter
{
  int i = 0;
  vector<int> vec;

  Iter(const vector<int>& _vec):
    vec(_vec)
  {
    cout << "copied vector of size "<<vec.size()<<" for iterator\n";
  }

  Iter(vector<int>&& _vec):
    vec(move(_vec))
  {
    cout << "moved vector of size "<<vec.size()<<" for iterator\n";
  }

  int& operator*() { return vec[i]; }
  Iter& operator++() { ++i; return *this; }
  bool operator!=(const Iter& _it) const { return false; }
};

struct Factory
{
  vector<int> fac_vec;

  Factory(const vector<int>& _v):
    fac_vec(_v)
  {
    cout << "copied vector of size " << fac_vec.size() << " for factory\n";
  }

  Factory(vector<int>&& _v):
    fac_vec(move(_v))
  {
    cout << "moved vector of size "<<fac_vec.size()<<" for factory\n";
  }

  Iter begin() { return Iter(fac_vec); }
  Iter end() { return Iter({}); }
};
int main(){
  for(const int i: Factory({1,2,3}))
    cout << i << "\n";
  return 0;
}

现在,运行这段代码给了我 (g++ 8.3):

moved vector of size 3 for factory   [ initialization of the factory with {1,2,3} - moved ]
copied vector of size 3 for iterator [ initialization of the begin-iterator with fac_vec - copied ]
moved vector of size 0 for iterator  [ initialization of the end-iterator with {} - moved ]

这有点令人失望,因为我希望最后一个 begin() 会调用 Iter 的移动构造函数,因为工厂将在之后立即被销毁(不是吗?)并且编译器拥有决定这一点所需的所有信息。

我想我可以使用 std::shared_ptr 做我想做的事,但这会在代码和程序中产生开销。我更愿意告诉编译器在调用最后一个 begin()move fac_vec 。有办法吗?

通常,在实现迭代器时,您采用向量的迭代器或对向量的引用:

struct Iter
{
  int i = 0;
  vector<int>* vec;

  Iter(vector<int>& _vec):
    vec(&_vec)
  {
    cout << "copied vector of size "<<vec->size()<<" for iterator\n";
  }

  int& operator*() { return (*vec)[i]; }
  Iter& operator++() { ++i; return *this; }
  bool operator!=(const Iter& _it) const { return false; }
};

这样你就永远不会复制。

更好,使用vector自带的迭代器:

struct Iter
{
  vector<int>::iterator it;

  Iter(vector<int>::iterator _it):
    it(_it)
  { }

  int& operator*() { return *it; }
  Iter& operator++() { ++it; return *this; }
  bool operator!=(const Iter& _it) const { return false; }
};

但是,您可以删除 Iter 并简单地使用向量迭代器:

struct Factory
{
  // ...    

  auto begin() { return fac_vec.begin(); }
  auto end() { return fac_vec.end(); }
};

现在如果你真的想在迭代器中包含一个值(不推荐)

编译器不会移动 fac_vec 因为它是向量的左值。这里不动。您需要向量的右值才能移动它。

您可以通过重载右值引用实例的函数来获得它:

struct Factory
{
  vector<int> fac_vec;

  Factory(const vector<int>& _v):
    fac_vec(_v)
  {
    cout << "copied vector of size " << fac_vec.size() << " for factory\n";
  }

  Factory(vector<int>&& _v):
    fac_vec(move(_v))
  {
    cout << "moved vector of size "<<fac_vec.size()<<" for factory\n";
  }

  // moving  when Factory is temporary
  Iter begin() && { return Iter(std::move(fac_vec)); }
  Iter end() && { return Iter({}); }

  // copying
  Iter begin() const& { return Iter(fac_vec); }
  Iter end() const& { return Iter({}); }
};

但是for循环不会调用move版本。为什么?这将是危险的,因为这需要对一个类型多次调用 move。

的范围大致相当于:

auto&& range = <range expr>; // forwarding reference to range (rvalue in your case)
auto begin = range.begin(); // range is lvalue, so calls the const&
auto end = range.end(); // range is lvalue, so calls the const&

for (/* ... */) {
    // body
}

要使用您的移动操作,需要多次将范围转换为右值,可能会使用移动自值:

auto begin = std::move(range).begin(); // range is lvalue but moved so calls the &&
auto end = std::move(range).end(); // range is lvalue but moved so calls the &&

如果您想在不推荐的迭代器中使用值,则不能使用范围 for 循环,必须使用旧式 for 循环。

请记住,像这样调用 move 通常会被视为代码味道,多次使用 move 更是如此。