如何检查范围内的元素是否应该移动?

How to check whether elements of a range should be moved?

有个类似的问题:check if elements of a range can be moved?

我不认为其中的答案是一个很好的解决方案。实际上,它需要对所有容器进行部分专业化。


我尝试了,但我不确定检查 operator*() 是否足够。

// RangeType

using IteratorType = std::iterator_t<RangeType>;
using Type = decltype(*(std::declval<IteratorType>()));

constexpr bool canMove = std::is_rvalue_reference_v<Type>;

更新

问题可以分为两部分:

  1. std::copy/std::uninitialized_copy 这样的 STL 算法在接收 r 值元素时真的可以避免不必要的深度复制吗?
  2. 当接收到范围的r值时,如何检查它是否是range adapter,如std::ranges::subrange,或持有其元素 所有权 的容器,如 std::vector?
template <typename InRange, typename OutRange>
void func(InRange&& inRange, OutRange&& outRange) {
    using std::begin;
    using std::end;
    std::copy(begin(inRange), end(inRange), begin(outRange));
    // Q1: if `*begin(inRange)` returns a r-value,
    //     would move-assignment of element be called instead of a deep copy?
}

std::vector<int> vi;
std::list<int> li;
/* ... */
func(std::move(vi), li2);
// Q2: Would elements be shallow copy from vi?
// And if not, how could I implement just limited count of overloads, without overload for every containers?
// (define a concept (C++20) to describe those who take ownership of its elements)

正如@Nicol Bolas、@eerorika 和@Davis Herring 指出的那样,Q1 不是问题,这不是我感到困惑的地方。 (但我确实认为 API 令人困惑,std::assign/std::uninitialized_construct 可能是更理想的名称)

@alfC 对我的问题 (Q2) 做出了很好的回答,并给出了一个原始的视角。 (为具有元素所有权的范围移动成语)


总而言之,对于大多数当前容器(尤其是来自 STL 的容器)(以及每个范围适配器...),所有容器的部分 specialization/overload 功能是唯一的解决方案,例如:

template <typename Range>
void func(Range&& range) { /*...*/ }

template <typename T>
void func(std::vector<T>&& movableRange) {
    auto movedRange = std::ranges::subrange{
        std::make_move_iterator(movableRange.begin()),
        std::make_move_iterator(movableRange.end())
    };

    func(movedRange);
}

// and also for `std::list`, `std::array`, etc...

我明白你的意思。 我确实认为这是一个真正的问题。

我的回答是,社区必须就移动嵌套对象(例如容器)的确切含义达成一致。 无论如何,这需要容器实现者的合作。 而且,如果是标准容器,规格也很好。

我对标准容器可以更改为“概括”“移动”的含义持悲观态度,但这不能阻止新的用户定义容器利用 move-idioms。 问题是据我所知还没有人深入研究过。

现在,std::move 似乎暗示“浅”移动(顶级“值类型”移动的一级)。 从某种意义上说,您可以移动整个事物,但不一定可以移动各个部分。 反过来,这使得尝试“std::move”non-owning 范围或提供 pointer/iterator 稳定性的范围毫无用处。

一些图书馆,例如与 std::ranges 相关的只是拒绝 r-value 的参考范围,我认为这只是踢罐头。

假设您有一个容器Bagstd::move(bag)[0]std::move(bag).begin()return应该是什么?这真的取决于容器的实施决定return。

很难想到通用的数据结构,位如果数据结构简单(例如动态数组)为了与structs保持一致(std::move(s).fieldstd::move(bag)[0]应该是与 std::move(bag[0]) 相同,但是标准强烈反对我已经在这里:https://en.cppreference.com/w/cpp/container/vector/operator_at 而且有可能来不及改变。

std::move(bag).begin() 也是如此,根据我的逻辑,应该 return 一个 move_iterator(或类似的东西)。

更糟糕的是,std::array<T, N> 的工作方式符合我的预期(std::move(arr[0]) 相当于 std::move(arr)[0])。 但是 std::move(arr).begin() 是一个简单的指针,所以它丢失了“forwarding/move”信息!一团糟。

所以,是的,要回答你的问题,你可以检查 using Type = decltype(*std::forward<Bag>(bag).begin()); 是否是 r-value,但通常它不会实现为 r-value。 也就是说,您必须抱最好的希望并相信 .begin* 是以非常具体的方式实现的。

通过(以某种方式)检查范围本身的类别,您的状态会更好。 也就是说,目前你只能靠自己的设备:如果你知道 bag 绑定到 r-value 并且类型在概念上是一个“拥有”值,你目前必须使用std::make_move_iterator.

我目前正在对我拥有的自定义容器进行大量试验。 https://gitlab.com/correaa/boost-multi 但是,通过尝试允许这样做,我打破了标准容器在移动方面的预期行为。 此外,一旦您处于 non-owning 范围的范围内,您必须使迭代器可以“手动”移动。

我发现凭经验区分 top-level 移动(std::move)和元素智能移动(例如 bag.mbegin()bag.moved().begin())很有用。 否则我会发现我的自我超载 std::move 如果有的话,这应该是最后的手段。

换句话说,在

template<class MyRange>
void f(MyRange&& r) {
   std::copy(std::forward<MyRange>(r).begin(), ..., ...);
}

r 绑定到 r-value 的事实并不一定意味着元素可以移动,因为 MyRange 可以只是一个 non-owning 视图一个“刚刚”生成的更大的容器。

因此,一般来说,您需要一种外部机制来检测 MyRange 是否拥有这些值,而不是像您建议的那样只检测 *std::forward<MyRange>(r).begin() 的“值类别”。

我想人们希望在未来使用范围来指示某种 adaptor-like 事物“std::ranges::moved_range”的深度移动,或者使用 3 参数 std::move

如果问题是使用 std::move 还是 std::copy(或 ranges:: 等价物),答案很简单:总是使用 copy。如果给你的范围有右值元素(,它的ranges::range_reference_t是右值的一种(!)),你无论如何都会从它们移动(只要目的地支持移动赋值)。

move 方便您拥有该范围并决定 移动其元素。

问题的答案是:不可能。至少对于当前的 STL 容器而言。


假设我们是否可以为 Container Requirements 添加一些限制?

添加一个静态常量isContainer,并创建一个RangeTraits。这可能很好用,但不是我想要的优雅解决方案。

受到@alfC 的启发,我正在考虑 r-value 容器本身的正确行为,这可能有助于提出一个概念 (C++20)。

有一种方法可以区分容器和范围适配器之间的区别,实际上,虽然由于当前实现的缺陷无法检测到,但语法设计上没有。


首先,元素的生命周期不能超过其容器,与范围适配器无关。

这意味着,从 r-value 容器中检索元素的地址(通过迭代器或引用)是错误的行为。


有一件事在post-11纪元经常被忽略,ref-qualifier.

许多现有成员函数,如 std::vector::swap,应标记为 l-value 合格:

auto getVec() -> std::vector<int>;

//
std::vector<int> vi1;
//getVec().swap(vi1); // pre-11 grammar, should be deprecated now
vi1 = getVec(); // move-assignment since C++11

但出于兼容性原因,并未采用。 (更令人困惑的是,ref-qualifier 还没有广泛应用于 newly-built,例如 std::arraystd::forward_list..)


例如,很容易实现我们预期的下标运算符:

template <typename T>
class MyArray {
    T* _items;
    size_t _size;
    /* ... */

public:
    T& operator [](size_t index) & {
        return _items[index];
    }
    const T& operator [](size_t index) const& {
        return _items[index];
    }

    T operator [](size_t index) && {
        // not return by `T&&` !!!
        return std::move(_items[index]);
    }

    // or use `deducing this` since C++23
};

Ok,那么std::move(container)[index]会return和std::move(container[index])一样的结果(不完全一样,可能会增加一个额外的move操作开销),这在我们尝试转发a时很方便容器。

但是,beginend 呢?


template <typename T>
class MyArray {
    T* _items;
    size_t _size;
    /* ... */

    class iterator;
    class const_iterator;
    using move_iterator = std::move_iterator<iterator>;

public:
    iterator begin() & { /*...*/ }
    const_iterator begin() const& { /*...*/ }

    // may works well with x-value, but pr-value?
    move_iterator begin() && {
        return std::make_move_iterator(begin());
    }

    // or more directly, using ADL
};

就这么简单吗?

不!迭代器将在容器销毁后失效。因此,从临时 (pr-value) 中引用迭代器是 未定义的行为!!

auto getVec() -> std::vector<int>;

///
auto it = getVec().begin(); // Noooo
auto item = *it; // undefined behaviour

由于(对于程序员)无法识别对象是 pr-value 还是 x-value(两者都将推导为 T),因此从 r-value容器应该是禁止.


如果我们可以规范 Container 的行为, 显式删除 从 [=90= 获取迭代器的函数]容器,那么就可以检测出来了。

这里有一个简单的演示: https://godbolt.org/z/4zeMG745f


在我看来,禁止这种明显错误的行为可能不会造成 well-implemented 旧项目无法编译的破坏性。

实际上,它只需要对每个容器进行一些修改,并为 std::begin/std::ranges::begin.

等范围访问实用程序添加适当的约束或重载