span 是否传播 const?
Does span propagate const?
标准容器传播常量。也就是说,如果容器本身是常量,则它们的元素自动是常量。例如:
const std::vector vec{3, 1, 4, 1, 5, 9, 2, 6};
ranges::fill(vec, 314); // impossible
const std::list lst{2, 7, 1, 8, 2, 8, 1, 8};
ranges::fill(lst, 272); // impossible
内置数组也传播常量:
const int arr[] {1, 4, 1, 4, 2, 1, 3, 5};
ranges::fill(arr, 141); // impossible
但是,我注意到 std::span
(大概)不会传播 const。最小可重现示例:
#include <algorithm>
#include <cassert>
#include <span>
namespace ranges = std::ranges;
int main()
{
int arr[] {1, 7, 3, 2, 0, 5, 0, 8};
const std::span spn{arr};
ranges::fill(spn, 173); // this compiles
assert(ranges::count(arr, 173) == 8); // passes
}
为什么这段代码可以正常工作?为什么 std::span
对待 const 的方式与标准容器不同?
想想指针。指针也不会传播 const。指针的常量与元素类型的常量无关。
考虑修改后的最小可重现示例:
#include <algorithm>
#include <cassert>
#include <span>
namespace ranges = std::ranges;
int main()
{
int var = 42;
int* const ptr{&var};
ranges::fill_n(ptr, 1, 84); // this also compiles
assert(var == 84); // passes
}
根据设计,std::span
是一种指向连续元素序列的指针。每 [span.iterators]:
constexpr iterator begin() const noexcept;
constexpr iterator end() const noexcept;
请注意,begin()
和 end()
return 一个 non-const 迭代器,无论 span 本身是否为 const。因此,std::span
不会以类似于指针的方式传播 const。跨度的常量与元素类型的常量无关。
const1 std::span<const2 ElementType, Extent>
第一个 const
指定跨度本身的常量。第二个 const
指定元素的常量。换句话说:
std::span< T> // non-const span of non-const elements
std::span<const T> // non-const span of const elements
const std::span< T> // const span of non-const elements
const std::span<const T> // const span of const elements
如果我们把例子中spn
的声明改成:
std::span<const int, 8> spn{arr};
代码无法编译,就像标准容器一样。在这方面,您是否将 spn
本身标记为 const 并不重要。 (但是,如果将其标记为 const,则不能执行 spn = another_arr
之类的操作)
(注意:您仍然可以在 std::as_const
的帮助下使用 class 模板参数推导:
std::span spn{std::as_const(arr)};
别忘了#include <utility>
。)
为 span
这样的类型传播 const 实际上没有多大意义,因为它无论如何也无法保护您免受任何伤害。
考虑:
void foo(std::span<int> const& s) {
// let's say we want this to be ill-formed
// that is, s[0] gives a int const& which
// wouldn't be assignable
s[0] = 42;
// now, consider what this does
std::span<int> t = s;
// and this
t[0] = 42;
}
即使s[0]
给出了int const&
,t[0]
肯定给出了int&
。而 t
指的是与 s
完全相同的元素。毕竟它是一个副本,span
不拥有它的元素——它是一个引用类型。即使 s[0] = 42
失败,std::span(s)[0] = 42
也会成功。这种限制对任何人都没有好处。
与常规容器(例如 vector
)的不同之处在于,此处的副本仍然引用相同的元素,而复制 vector
会为您提供全新的元素。
让 span
引用不可变元素的方法不是让 span
本身成为 const
,而是让底层元素本身成为 const
。即:span<T const>
,而不是span<T> const
。
标准容器传播常量。也就是说,如果容器本身是常量,则它们的元素自动是常量。例如:
const std::vector vec{3, 1, 4, 1, 5, 9, 2, 6};
ranges::fill(vec, 314); // impossible
const std::list lst{2, 7, 1, 8, 2, 8, 1, 8};
ranges::fill(lst, 272); // impossible
内置数组也传播常量:
const int arr[] {1, 4, 1, 4, 2, 1, 3, 5};
ranges::fill(arr, 141); // impossible
但是,我注意到 std::span
(大概)不会传播 const。最小可重现示例:
#include <algorithm>
#include <cassert>
#include <span>
namespace ranges = std::ranges;
int main()
{
int arr[] {1, 7, 3, 2, 0, 5, 0, 8};
const std::span spn{arr};
ranges::fill(spn, 173); // this compiles
assert(ranges::count(arr, 173) == 8); // passes
}
为什么这段代码可以正常工作?为什么 std::span
对待 const 的方式与标准容器不同?
想想指针。指针也不会传播 const。指针的常量与元素类型的常量无关。
考虑修改后的最小可重现示例:
#include <algorithm>
#include <cassert>
#include <span>
namespace ranges = std::ranges;
int main()
{
int var = 42;
int* const ptr{&var};
ranges::fill_n(ptr, 1, 84); // this also compiles
assert(var == 84); // passes
}
根据设计,std::span
是一种指向连续元素序列的指针。每 [span.iterators]:
constexpr iterator begin() const noexcept; constexpr iterator end() const noexcept;
请注意,begin()
和 end()
return 一个 non-const 迭代器,无论 span 本身是否为 const。因此,std::span
不会以类似于指针的方式传播 const。跨度的常量与元素类型的常量无关。
const1 std::span<const2 ElementType, Extent>
第一个 const
指定跨度本身的常量。第二个 const
指定元素的常量。换句话说:
std::span< T> // non-const span of non-const elements
std::span<const T> // non-const span of const elements
const std::span< T> // const span of non-const elements
const std::span<const T> // const span of const elements
如果我们把例子中spn
的声明改成:
std::span<const int, 8> spn{arr};
代码无法编译,就像标准容器一样。在这方面,您是否将 spn
本身标记为 const 并不重要。 (但是,如果将其标记为 const,则不能执行 spn = another_arr
之类的操作)
(注意:您仍然可以在 std::as_const
的帮助下使用 class 模板参数推导:
std::span spn{std::as_const(arr)};
别忘了#include <utility>
。)
为 span
这样的类型传播 const 实际上没有多大意义,因为它无论如何也无法保护您免受任何伤害。
考虑:
void foo(std::span<int> const& s) {
// let's say we want this to be ill-formed
// that is, s[0] gives a int const& which
// wouldn't be assignable
s[0] = 42;
// now, consider what this does
std::span<int> t = s;
// and this
t[0] = 42;
}
即使s[0]
给出了int const&
,t[0]
肯定给出了int&
。而 t
指的是与 s
完全相同的元素。毕竟它是一个副本,span
不拥有它的元素——它是一个引用类型。即使 s[0] = 42
失败,std::span(s)[0] = 42
也会成功。这种限制对任何人都没有好处。
与常规容器(例如 vector
)的不同之处在于,此处的副本仍然引用相同的元素,而复制 vector
会为您提供全新的元素。
让 span
引用不可变元素的方法不是让 span
本身成为 const
,而是让底层元素本身成为 const
。即:span<T const>
,而不是span<T> const
。