clang-tidy: `循环变量被复制但仅用作 const 引用;考虑将其设为 const reference` - 这真的很重要吗?
clang-tidy: `Loop variable is copied but only used as const reference; consider making it a const reference` - does it really matter?
我正在处理 clang-tidy 到处标记的代码
Loop variable is copied but only used as const reference; consider making it a const reference
当前代码:
for (auto foo : collection) {
...
}
clang-tidy 建议我使用什么:
for (const auto &foo : collection) {
...
}
我当然可以看到 const 引用在哪里比实际复制值更有效,但我想知道编译器是否会以这种方式优化它?我不想徒劳地深入研究并更改数百个循环。
除了构造新对象之外,构造函数还会产生副作用。因此,两个版本的语义可能不同。
即使不是这种情况,编译器也可能无法在编译期间确定这一点。例如,如果 foo
类型的复制构造函数没有在同一个翻译单元中定义并且没有使用 link-time 优化,编译器将无法优化掉副本,因为它在当前翻译单元的编译时可能有未知的副作用。
同样,循环体可能会调用编译器无法证明它们没有修改的函数 foo
,在这种情况下,也无法优化副本。
(顺便说一句,编译器比 clang-tidy 给你警告更难证明,因为从技术上讲,即使是采用 foo
的 const
引用的函数也是如此仍然允许修改它。即使 foo
对象本身是 const
限定的(例如 const auto
),函数也可能取决于 foo
对象的不同地址比容器的 foo
对象通过其他程序路径。)
即使翻译单元中的所有内容都是可见的,并且可观察到的行为不依赖于副本,操作也可能过于复杂,编译器无法优化副本。
示例:
auto f(const std::vector<std::string>& x) {
std::size_t n = 0;
for(auto y : x)
n += y.size();
return n;
}
带有 -O3
和 libstdc++ 的 GCC 和 Clang 都不会优化副本(请参阅程序集中的 operator new
/operator delete
/memcpy
调用): https://godbolt.org/z/d66ac617M
也用引用来比较相同的代码:https://godbolt.org/z/Tdc3GhcEv
但是,在上面的示例中,std::string
的标准库实现可能仍然存在可见性问题。这可能是一个更好的例子,其中所有定义可能在当前翻译单元中可见:
struct A { int a[64]; };
auto f(const std::vector<std::vector<A>>& x) {
std::size_t n = 0;
for(auto y : x)
n += y.size();
return n;
}
在这种情况下,GCC 仍然没有优化副本,而 Clang 做了,尽管出于某种原因它仍然在那里留下一些分配大小检查:https://godbolt.org/z/8KWPTx4o5
但是,如果类型变得更复杂,那么即使一切都可见,编译器也可能无法优化掉副本。
如果你知道 foo
的类型很简单,例如像 std::vector<int>
容器上的标量,那么它在性能方面并不重要,假设可观察到的行为是相同的。
您可能仍想使用 const
引用,更具体地说是 const
部分,以便在整个代码中保持常量正确性。这有多么必要,不过可能会进入基于意见的领域。
我正在处理 clang-tidy 到处标记的代码
Loop variable is copied but only used as const reference; consider making it a const reference
当前代码:
for (auto foo : collection) {
...
}
clang-tidy 建议我使用什么:
for (const auto &foo : collection) {
...
}
我当然可以看到 const 引用在哪里比实际复制值更有效,但我想知道编译器是否会以这种方式优化它?我不想徒劳地深入研究并更改数百个循环。
除了构造新对象之外,构造函数还会产生副作用。因此,两个版本的语义可能不同。
即使不是这种情况,编译器也可能无法在编译期间确定这一点。例如,如果 foo
类型的复制构造函数没有在同一个翻译单元中定义并且没有使用 link-time 优化,编译器将无法优化掉副本,因为它在当前翻译单元的编译时可能有未知的副作用。
同样,循环体可能会调用编译器无法证明它们没有修改的函数 foo
,在这种情况下,也无法优化副本。
(顺便说一句,编译器比 clang-tidy 给你警告更难证明,因为从技术上讲,即使是采用 foo
的 const
引用的函数也是如此仍然允许修改它。即使 foo
对象本身是 const
限定的(例如 const auto
),函数也可能取决于 foo
对象的不同地址比容器的 foo
对象通过其他程序路径。)
即使翻译单元中的所有内容都是可见的,并且可观察到的行为不依赖于副本,操作也可能过于复杂,编译器无法优化副本。
示例:
auto f(const std::vector<std::string>& x) {
std::size_t n = 0;
for(auto y : x)
n += y.size();
return n;
}
带有 -O3
和 libstdc++ 的 GCC 和 Clang 都不会优化副本(请参阅程序集中的 operator new
/operator delete
/memcpy
调用): https://godbolt.org/z/d66ac617M
也用引用来比较相同的代码:https://godbolt.org/z/Tdc3GhcEv
但是,在上面的示例中,std::string
的标准库实现可能仍然存在可见性问题。这可能是一个更好的例子,其中所有定义可能在当前翻译单元中可见:
struct A { int a[64]; };
auto f(const std::vector<std::vector<A>>& x) {
std::size_t n = 0;
for(auto y : x)
n += y.size();
return n;
}
在这种情况下,GCC 仍然没有优化副本,而 Clang 做了,尽管出于某种原因它仍然在那里留下一些分配大小检查:https://godbolt.org/z/8KWPTx4o5
但是,如果类型变得更复杂,那么即使一切都可见,编译器也可能无法优化掉副本。
如果你知道 foo
的类型很简单,例如像 std::vector<int>
容器上的标量,那么它在性能方面并不重要,假设可观察到的行为是相同的。
您可能仍想使用 const
引用,更具体地说是 const
部分,以便在整个代码中保持常量正确性。这有多么必要,不过可能会进入基于意见的领域。