为什么标准范围算法 return std::ranges::dangling 用于右值参数而不是......好吧,只是工作?
Why do std range algorithms return std::ranges::dangling for rvalue arguments instead of... well, just working?
这是我(简化的)尝试实现一个适用于左值和右值参数的 ranges::min_element
版本:
#include <iterator>
#include <algorithm>
#include <type_traits>
#include <utility>
namespace better_std_ranges
{
template<typename Range>
constexpr auto min_element(Range& range)
{
using std::begin;
using std::end;
return std::min_element(begin(range), end(range));
}
template<typename Range>
constexpr auto min_element(Range&& range)
{
static_assert(!std::is_reference_v<Range>, "wrong overload chosen");
class _result_iterator_type // todo: inherit from some crtp base that will provide lacking operators depending on _underlying_iterator_type::iterator_category
{
using _underlying_iterator_type = std::decay_t<decltype(std::begin(std::declval<Range&>()))>;
public:
explicit constexpr _result_iterator_type(Range&& range) noexcept(std::is_nothrow_move_constructible_v<Range>)
: _underlying_range{std::move(range)}
, _underlying_iterator(::better_std_ranges::min_element(_underlying_range))
{
}
using difference_type = typename _underlying_iterator_type::difference_type;
using value_type = typename _underlying_iterator_type::value_type;
using pointer = typename _underlying_iterator_type::pointer;
using reference = typename _underlying_iterator_type::reference;
using iterator_category = typename _underlying_iterator_type::iterator_category;
constexpr decltype(auto) operator*() const
{
return *_underlying_iterator;
}
// todo: define other member functions that were not provided by the inheritance above
private:
Range _underlying_range;
_underlying_iterator_type _underlying_iterator;
};
return _result_iterator_type{std::move(range)};
}
}
#include <vector>
#include <iostream>
auto make_vector()
{
return std::vector{100, 200, 42, 500, 1000};
}
int main()
{
auto lvalue_vector = make_vector();
auto lvalue_vector_min_element_iterator = better_std_ranges::min_element(lvalue_vector);
std::cout << *lvalue_vector_min_element_iterator << '\n';
auto rvalue_vector_min_element_iterator = better_std_ranges::min_element(make_vector());
std::cout << *rvalue_vector_min_element_iterator << '\n';
}
输出为
42
42
当然它缺少一些实现细节,但思路必须清晰:如果输入范围是右值,return 值可以存储它的移动副本。
因此,std::ranges
算法必须完全有可能使用右值参数。
我的问题是:为什么标准采取相反的方式,通过引入奇怪的 std::ranges::dangling
占位符来禁止在其算法中使用右值范围?
这种方法有两个问题。
首先,它破坏了算法的语义。 min_element
(以及任何其他 return 迭代器的算法)的要点是 return 迭代器 进入 范围。您没有这样做 - 您是 return 将迭代器放入不同的范围。这真的混淆了 return 在这种情况下甚至意味着什么的概念。您甚至会将此迭代器与什么进行比较?没有对应的.end()
?
其次,C++ 中的迭代器模型非常强烈地基于迭代器复制成本低的概念。每个算法都按值获取迭代器并自由复制它们。迭代器被认为是轻量级的,重要的是,它是非拥有的。对于正向迭代器,假定迭代器的副本是可互换的。
如果你突然有了一个迭代器,它引用了 member std::vector<T>
,那么一切都会崩溃。复制迭代器变得非常昂贵。现在每个不同的迭代器副本实际上是完全不同范围内的迭代器?
您可以通过让迭代器具有成员 std::shared_ptr<std::vector<T>>
而不是 std::vector<T>
来做得更好一点。这样副本就便宜得多并且不再独立,所以你有更接近合法迭代器的东西。但是现在您必须进行额外的分配(以创建共享指针),您仍然遇到问题,您正在 returning 的迭代器与给定的算法处于不同的范围内,并且您遇到了问题根据您提供的是左值还是右值范围,算法具有非常不同的语义。
基本上,min_element
右值范围需要:
- 只是 return 一个进入范围的迭代器,即使它会悬挂
- return 围绕这种潜在悬空迭代器的某种包装器(这是最初的 Ranges 设计,
dangling<I>
仍然可以让你到达底层 I
)
- return 某种类型表明这不起作用(当前设计)
- 如果使用会导致悬挂(Rust 允许的),则无法完全编译
我认为这里没有其他选择,真的。
这是我(简化的)尝试实现一个适用于左值和右值参数的 ranges::min_element
版本:
#include <iterator>
#include <algorithm>
#include <type_traits>
#include <utility>
namespace better_std_ranges
{
template<typename Range>
constexpr auto min_element(Range& range)
{
using std::begin;
using std::end;
return std::min_element(begin(range), end(range));
}
template<typename Range>
constexpr auto min_element(Range&& range)
{
static_assert(!std::is_reference_v<Range>, "wrong overload chosen");
class _result_iterator_type // todo: inherit from some crtp base that will provide lacking operators depending on _underlying_iterator_type::iterator_category
{
using _underlying_iterator_type = std::decay_t<decltype(std::begin(std::declval<Range&>()))>;
public:
explicit constexpr _result_iterator_type(Range&& range) noexcept(std::is_nothrow_move_constructible_v<Range>)
: _underlying_range{std::move(range)}
, _underlying_iterator(::better_std_ranges::min_element(_underlying_range))
{
}
using difference_type = typename _underlying_iterator_type::difference_type;
using value_type = typename _underlying_iterator_type::value_type;
using pointer = typename _underlying_iterator_type::pointer;
using reference = typename _underlying_iterator_type::reference;
using iterator_category = typename _underlying_iterator_type::iterator_category;
constexpr decltype(auto) operator*() const
{
return *_underlying_iterator;
}
// todo: define other member functions that were not provided by the inheritance above
private:
Range _underlying_range;
_underlying_iterator_type _underlying_iterator;
};
return _result_iterator_type{std::move(range)};
}
}
#include <vector>
#include <iostream>
auto make_vector()
{
return std::vector{100, 200, 42, 500, 1000};
}
int main()
{
auto lvalue_vector = make_vector();
auto lvalue_vector_min_element_iterator = better_std_ranges::min_element(lvalue_vector);
std::cout << *lvalue_vector_min_element_iterator << '\n';
auto rvalue_vector_min_element_iterator = better_std_ranges::min_element(make_vector());
std::cout << *rvalue_vector_min_element_iterator << '\n';
}
输出为
42
42
当然它缺少一些实现细节,但思路必须清晰:如果输入范围是右值,return 值可以存储它的移动副本。
因此,std::ranges
算法必须完全有可能使用右值参数。
我的问题是:为什么标准采取相反的方式,通过引入奇怪的 std::ranges::dangling
占位符来禁止在其算法中使用右值范围?
这种方法有两个问题。
首先,它破坏了算法的语义。 min_element
(以及任何其他 return 迭代器的算法)的要点是 return 迭代器 进入 范围。您没有这样做 - 您是 return 将迭代器放入不同的范围。这真的混淆了 return 在这种情况下甚至意味着什么的概念。您甚至会将此迭代器与什么进行比较?没有对应的.end()
?
其次,C++ 中的迭代器模型非常强烈地基于迭代器复制成本低的概念。每个算法都按值获取迭代器并自由复制它们。迭代器被认为是轻量级的,重要的是,它是非拥有的。对于正向迭代器,假定迭代器的副本是可互换的。
如果你突然有了一个迭代器,它引用了 member std::vector<T>
,那么一切都会崩溃。复制迭代器变得非常昂贵。现在每个不同的迭代器副本实际上是完全不同范围内的迭代器?
您可以通过让迭代器具有成员 std::shared_ptr<std::vector<T>>
而不是 std::vector<T>
来做得更好一点。这样副本就便宜得多并且不再独立,所以你有更接近合法迭代器的东西。但是现在您必须进行额外的分配(以创建共享指针),您仍然遇到问题,您正在 returning 的迭代器与给定的算法处于不同的范围内,并且您遇到了问题根据您提供的是左值还是右值范围,算法具有非常不同的语义。
基本上,min_element
右值范围需要:
- 只是 return 一个进入范围的迭代器,即使它会悬挂
- return 围绕这种潜在悬空迭代器的某种包装器(这是最初的 Ranges 设计,
dangling<I>
仍然可以让你到达底层I
) - return 某种类型表明这不起作用(当前设计)
- 如果使用会导致悬挂(Rust 允许的),则无法完全编译
我认为这里没有其他选择,真的。