我正在写一个进行数值计算的库。我正在使用模板,以便最终用户可以选择他们想要的精度。我希望这既适用于基本类型(double
,float
)又适用于高精度类类型(例如boost::multiprecision
)。我想知道参数类型应该为T
还是const & T
。
在SO / google上,有很多关于按价值传递和按引用传递的文章。 “经验法则”之一似乎是:
但是,如果您有模板,这会变得很混乱:
template<typename T>
T doSomething(T x, T y)
{
return x + y;
}
vs。
template<typename T>
T doSomething(const T & x, const T & y)
{
return x + y;
}
对于boost::multiprecision
,您几乎可以肯定要通过const引用传递。问题是,将double
传递给const &
是否比按值传递更糟。许多SO答案都说const &
“没有更好,也许还更糟”……但是我找不到任何好的硬参考。
我做了以下benchmark
这似乎没有什么区别,尽管可能取决于函数的简单性和内联行为。
有可能做类似的事情:
#include <type_traits>
template<typename T>
using choose_arg_type =
typename std::conditional<std::is_fundamental<T>::value,
T,
const T &>::type;
template <typename T>
T someFunc(choose_arg_type<T> arg)
{
return arg + arg;
}
int main()
{
auto result = someFunc<double>(0.0);
return 0;
}
但是,如果它没有带来任何好处,则会增加复杂性,并且您会丢失类型推断(Any way to fix this type deduction?)
我可以认为通过const引用传递的速度较慢的一个原因是,如果它确实在使用引用,则可能存在缓存局部性问题。但是,如果编译器只是优化价值……那就没关系了。
处理此问题的最佳方法是什么?
答案 0 :(得分:3)
在所讨论的基本类型适合寄存器的平台上,一个体面的编译器如果可以看到调用的两面,则应从参数中删除const引用。对于通常是给定的模板(除非在某处显式实例化了模板)。由于您的库大概必须一直被模板化,因此这将适用于您的情况。
您的最终用户可能会在以下情况下使用错误的编译器或平台: double
不适合寄存器。我不明白为什么会激励您为这些特定用户进行微优化,但是也许您会这么做。
也可能要为某些类型的集合显式实例化库中的所有模板,并提供无需实现的头文件。在这种情况下,用户的编译器必须遵守该平台上存在的所有调用约定,并且可能会通过引用传递基本类型。
如果您不相信编译器,那么答案是“概要介绍相关的和具有代表性的用例”。
编辑(已删除宏解决方案):如Jarod42所建议,C ++方式将使用别名模板。这也避免了缺少推论者与他们的original approach碰到的推论:
template<class T>
using CONSTREF = const T&; // Or just T for benchmarking.
推导模板模板参数时,绝不会通过模板参数推导来推导别名模板。
答案 1 :(得分:3)
在至少一种情况下,通过const
引用传递可能会禁用优化。但是,最受欢迎的编译器提供了一种重新启用它们的方法。
让我们看看这个功能:
int cryptographicHash( int& salt, const int& plaintext )
{
salt = 4; // Chosen by fair dice roll
// guaranteed to be random
return plaintext; // If we tell them there's a salt,
// this is the last hash function they'll
// ever suspect!
}
看起来很安全,对吧?但是,既然我们用C ++编写,它会尽可能快吗? (绝对是我们想要的加密哈希值。)
不,因为如果用以下方式调用它会发生什么:
int x = 0xFEED;
const int y = cryptographicHash( x, x );
现在,由引用传递的参数别名是同一对象,因此该函数应按书面形式返回4
,而不是0xFEED
。这意味着灾难性地,编译器无法再优化其&
参数中的const int&
。
但是,最流行的编译器(包括GCC,clang,Intel C ++和Visual C++ 2015 and up)都支持__restrict
扩展名。因此,将函数签名更改为int cryptographicHash( int& salt, const int& __restrict plaintext )
并永久解决所有问题。
由于此扩展名不属于C ++标准,因此您可以通过以下方式提高可移植性:
#if ( __GNUC__ || __clang__ || __INTEL_COMPILER || __ICL || _MSC_VER >= 1900 )
# define RESTRICT __restrict
#else
# define RESTRICT /**/
#endif
int cryptographicHash( int& salt, const int& RESTRICT plaintext );
(在GCC和clang中,这似乎不会更改生成的代码。)
答案 2 :(得分:1)
通过引用(基本上是一个指针)传递类似int
之类的东西显然是次优的,因为通过指针进行的额外间接访问可能会导致高速缓存未命中,并且由于编译器无法始终知道,因此也可能阻止编译器优化指向的变量不能由其他实体更改,因此在某些情况下可能被迫从内存中进行其他加载。按值传递将删除间接寻址,并使编译器假定没有其他人在更改该值。
答案 3 :(得分:0)
如果自变量是微不足道的构造且未修改,则按值传递。调用约定将自动通过引用传递大型结构。
struct alignas(4096) page {unsigned char bytes[4096];};
[[nodiscard]] constexpr page operator^(page l, page r) noexcept {
for (int i = 0; i < 4096; ++i)
l.bytes[i] = l.bytes[i] ^ r.bytes[i];
return l;
}
由非常量引用修改和/或返回的参数必须由非常量引用传递。
constexpr page& operator^=(page& l, page r) noexcept {return l = l ^ r;}
通过const引用传递以const引用语义返回的任何参数。
using buffer = std::vector<unsigned char>;
[[nodiscard]] std::string_view to_string_view(const buffer& b) noexcept {
return {reinterpret_cast<const char*>(b.data()), b.size()};
}
将通过const引用深度复制为其他类型的任何参数传递。
[[nodiscard]] std::string to_string(const buffer& b) {
return std::string{to_string_view(b)};
}
通过const引用传递任何非平凡可构造,未修改且非深度复制的参数。
std::ostream& operator<<(std::ostream& os, const buffer& b) {
os << std::hex;
for (const unsigned short u8 : b)
os << u8 << ',';
return os << std::dec;
}
按值传递任何深度复制到相同类型值的参数。通过引用传递一个无论如何都已复制的参数是没有意义的,并且返回的副本的构造函数已被优化。参见https://en.cppreference.com/w/cpp/language/copy_elision
[[nodiscard]] buffer operator^(buffer l, const buffer& r) {
const auto lsize = l.size();
const auto rsize = r.size();
const auto minsize = std::min(lsize, rsize);
for (buffer::size_type i = 0; i < minsize; ++i)
l[i] = l[i] ^ r[i];
if (lsize < rsize)
l.insert(l.end(), r.begin() + minsize, r.end());
return l;
}
这也包括模板功能。
template<typename T>
[[nodiscard]]
constexpr T clone(T t) noexcept(std::is_nothrow_constructible_v<T, T>) {
return t;
}
否则,通过转发引用(&&
)获取模板参数类型的参数。注意:&&
仅在模板参数类型的参数中和/或对于auto&&
或decltype(auto)&&
具有转发(通用)引用语义。
template<typename T>
constexpr bool nt = noexcept(std::is_nothrow_constructible_v<int, T&&>);
template<typename T>
[[nodiscard]]
constexpr int to_int(T&& t) noexcept(nt<T>) {return static_cast<int>(t);}
const auto to_int_lambda = [](auto&& t) noexcept(to_int(t)) {return to_int(t);};
答案 4 :(得分:0)
这是一个复杂的问题,取决于体系结构,编译器优化和许多其他细节,如答案所示。由于OP是关于编写模板函数的,所以还可以选择使用SFINAE来控制调用哪个函数。
#include <iostream>
template <typename T, typename = typename std::enable_if_t<std::is_fundamental_v<T>> >
void f(T t) {
std::cout << "Pass by value\n";
}
template <typename T, typename = typename std::enable_if_t<not std::is_fundamental_v<T>> >
void f(T const &t) {
std::cout << "Pass by const ref.\n";
}
class myclass {};
int main() {
float x;
int i;
myclass c;
std::cout << "float: ";
f(x);
std::cout << "int: ";
f(i);
std::cout << "myclass: ";
f(c);
return 0;
}
输出:
float: Pass by value
int: Pass by value
myclass: Pass by const ref.