实现可变参数 min / max 函数
Posted
技术标签:
【中文标题】实现可变参数 min / max 函数【英文标题】:Implementing variadic min / max functions 【发布时间】:2014-07-11 23:33:32 【问题描述】:我正在实现可变最小/最大函数。目标是利用编译时已知数量的参数并执行展开评估(避免运行时循环)。代码当前状态如下(呈现min-max类似)
#include <iostream>
using namespace std;
template<typename T>
T vmin(T val1, T val2)
return val1 < val2 ? val1 : val2;
template<typename T, typename... Ts>
T vmin(T val1, T val2, Ts&&... vs)
return val1 < val2 ?
vmin(val1, std::forward<Ts>(vs)...) :
vmin(val2, std::forward<Ts>(vs)...);
int main()
cout << vmin(3, 2, 1, 2, 5) << endl;
cout << vmin(3., 1.2, 1.3, 2., 5.2) << endl;
return 0;
现在this works,但我有一些疑问/问题:
非可变参数重载必须按值接受其参数。如果我尝试传递其他类型的 ref,我会得到以下结果
通用引用&&
-> 编译错误
const 引用 const&
-> 好的
普通引用&
-> 编译错误
现在我知道了function templates mix weirdly with templates,但是手头的混合有什么具体的诀窍吗? 我应该选择哪种类型的论据?
参数包的扩展不够吗?我真的需要将我的论点转发给递归调用吗?
当包装在结构中并作为静态成员函数公开时,此功能是否可以更好地实现。部分专业化的能力能给我带来什么吗?
功能版本是否有更健壮/高效的实现/设计? (特别是我想知道constexpr
版本是否与模板元编程的效率相匹配)
【问题讨论】:
好问题。你甚至可以像this 一样实现min
。
为什么不使用std::min
和std::initializer_list
,即template <typename ...Args> auto vmin(Args&&... args) return std::min( std::forward<Args>(args)... );
?
@nosid 好的点,但我正在尝试(同样它可能没有太大意义)实现一个执行编译时展开的版本,而不是在运行时运行循环的函数(也许我应该在问题中提到这一点 - 我只是注意到它并不明显)
@nosid 我认为你应该在你的变体中使用std::common_type
之类的东西。
考虑按照vmin(vmin(val1, val2), vs...)
的方式实现。
【参考方案1】:
live example
这对参数进行了完美的转发。它依赖于 RVO 的返回值,因为无论输入类型如何,它都会返回一个值类型,因为 common_type
就是这样做的。
我实现了common_type
推导,允许传入混合类型,输出“预期”的结果类型。
我们支持 1 个元素的最小值,因为它使代码更流畅。
#include <utility>
#include <type_traits>
template<typename T>
T vmin(T&&t)
return std::forward<T>(t);
template<typename T0, typename T1, typename... Ts>
typename std::common_type<
T0, T1, Ts...
>::type vmin(T0&& val1, T1&& val2, Ts&&... vs)
if (val2 < val1)
return vmin(val2, std::forward<Ts>(vs)...);
else
return vmin(val1, std::forward<Ts>(vs)...);
int main()
std::cout << vmin(3, 2, 0.9, 2, 5) << std::endl;
std::cout << vmin(3., 1.2, 1.3, 2., 5.2) << std::endl;
return 0;
现在,虽然以上是完全可以接受的解决方案,但并不理想。
表达式 ((a<b)?a:b) = 7
是合法的 C++,但 vmin( a, b ) = 7
不是,因为 std::common_type
decay
s 是盲目的参数(由于我认为它在输入两个值时返回右值引用的过度反应 -在 std::common_type
的旧实现中键入)。
简单地使用decltype( true?a:b )
很诱人,但它会导致右值引用问题,并且不支持common_type
特化(例如std::chrono
)。所以我们都想用common_type
,又不想用。
其次,写一个min
函数,不支持不相关的指针,不让用户改变比较函数似乎是错误的。
所以下面是上面的一个更复杂的版本。 live example:
#include <iostream>
#include <utility>
#include <type_traits>
namespace my_min
// a common_type that when fed lvalue references all of the same type, returns an lvalue reference all of the same type
// however, it is smart enough to also understand common_type specializations. This works around a quirk
// in the standard, where (true?x:y) is an lvalue reference, while common_type< X, Y >::type is not.
template<typename... Ts>
struct my_common_type;
template<typename T>
struct my_common_type<T>typedef T type;;
template<typename T0, typename T1, typename... Ts>
struct my_common_type<T0, T1, Ts...>
typedef typename std::common_type<T0, T1>::type std_type;
// if the types are the same, don't change them, unlike what common_type does:
typedef typename std::conditional< std::is_same< T0, T1 >::value,
T0,
std_type >::type working_type;
// Careful! We do NOT want to return an rvalue reference. Just return T:
typedef typename std::conditional<
std::is_rvalue_reference< working_type >::value,
typename std::decay< working_type >::type,
working_type
>::type common_type_for_first_two;
// TODO: what about Base& and Derived&? Returning a Base& might be the right thing to do.
// on the other hand, that encourages silent slicing. So maybe not.
typedef typename my_common_type< common_type_for_first_two, Ts... >::type type;
;
template<typename... Ts>
using my_common_type_t = typename my_common_type<Ts...>::type;
// not that this returns a value type if t is an rvalue:
template<typename Picker, typename T>
T pick(Picker&& /*unused*/, T&&t)
return std::forward<T>(t);
// slight optimization would be to make Picker be forward-called at the actual 2-arg case, but I don't care:
template<typename Picker, typename T0, typename T1, typename... Ts>
my_common_type_t< T0, T1, Ts...> pick(Picker&& picker, T0&& val1, T1&& val2, Ts&&... vs)
// if picker doesn't prefer 2 over 1, use 1 -- stability!
if (picker(val2, val1))
return pick(std::forward<Picker>(pick), val2, std::forward<Ts>(vs)...);
else
return pick(std::forward<Picker>(pick), val1, std::forward<Ts>(vs)...);
// possibly replace with less<void> in C++1y?
struct lesser
template<typename LHS, typename RHS>
bool operator()( LHS&& lhs, RHS&& rhs ) const
return std::less< typename std::decay<my_common_type_t<LHS, RHS>>::type >()(
std::forward<LHS>(lhs), std::forward<RHS>(rhs)
);
;
// simply forward to the picked_min function with a smart less than functor
// note that we support unrelated pointers!
template<typename... Ts>
auto min( Ts&&... ts )->decltype( pick( lesser(), std::declval<Ts>()... ) )
return pick( lesser(), std::forward<Ts>(ts)... );
int main()
int x = 7;
int y = 3;
int z = -1;
my_min::min(x, y, z) = 2;
std::cout << x << "," << y << "," << z << "\n";
std::cout << my_min::min(3, 2, 0.9, 2, 5) << std::endl;
std::cout << my_min::min(3., 1.2, 1.3, 2., 5.2) << std::endl;
return 0;
上述实现的缺点是大多数类不支持operator=(T const&)&&=delete
- 即,它们不会阻止分配右值,如果min
中的一种类型不支持,这可能会导致意外.基本类型可以。
附注:开始删除您的右值引用operator=
s 人。
【讨论】:
嗯。尽管对common_type
的天真阅读说common_type<X,Y>::type
是true?std::declval<X>():std::declval<Y>()
的类型,但似乎common_type<int&, int&>::type
不是int&
而是int
。奇怪。
我认为基本情况应该有一个参数,这也消除了将T2&& v0
与参数包分开的需要。见ideone.com/5J6BBw
哦,更好的是,完全失去std::common_type
。见ideone.com/OcQZ06
@Yakk 我认为是因为common_type<int&, float&>::type
不能是float&
。
Stepanov 认为min(a, b)
应该返回a
iff a <= b
,即b < a ? b : a
。与“自然秩序”有关,与稳定性有关。本系列某处:youtube.com/watch?v=aIHAEYyoTUc【参考方案2】:
我很欣赏 Yakk 对返回类型的想法,所以我不必这样做,但它变得简单多了:
template<typename T>
T&& vmin(T&& val)
return std::forward<T>(val);
template<typename T0, typename T1, typename... Ts>
auto vmin(T0&& val1, T1&& val2, Ts&&... vs)
return (val1 < val2) ?
vmin(val1, std::forward<Ts>(vs)...) :
vmin(val2, std::forward<Ts>(vs)...);
返回类型推导非常棒(可能需要 C++14)。
【讨论】:
"可能需要 C++14" 它确实需要 C++1y。我想知道decltype(auto)
在这里是否是一个更好的选择。另外,***.com/questions/23815138/…
传递两个不同的 std::chrono
类型:common_type
比 ?
更聪明
@Yakk:但是哪个结果“更好”? vmin
是否有必要在 ?:
没有的类型上工作?
@BenVoigt:一个问题,在这种情况下&&
与const &
相比有什么优势,而min
不复制或修改输入?像this一样通过const &
(不需要完美转发)传递输入不是更好吗?
@MM。正如我在对您的问题的评论中所说,这些论点被复制/移动。 auto
返回类型推导和模板类型推导一样,从不推导引用,因此该函数按值返回。 (因此我建议使用decltype(auto)
。)【参考方案3】:
4) 这是实现此函数的constexpr
版本的一种可能方法:
#include <iostream>
#include <type_traits>
template <typename Arg1, typename Arg2>
constexpr typename std::common_type<Arg1, Arg2>::type vmin(Arg1&& arg1, Arg2&& arg2)
return arg1 < arg2 ? std::forward<Arg1>(arg1) : std::forward<Arg2>(arg2);
template <typename Arg, typename... Args>
constexpr typename std::common_type<Arg, Args...>::type vmin(Arg&& arg, Args&&... args)
return vmin(std::forward<Arg>(arg), vmin(std::forward<Args>(args)...));
int main()
std::cout << vmin(3, 2, 1, 2, 5) << std::endl;
std::cout << vmin(3., 1.2, 1.3, 2., 5.2) << std::endl;
见live example。
编辑:正如 @Yakk 在 cmets 中指出的那样,代码 std::forward<Arg1>(arg1) < std::forward<Arg2>(arg2) ? std::forward<Arg1>(arg1) : std::forward<Arg2>(arg2)
在某些情况下可能会导致问题。 arg1 < arg2 ? std::forward<Arg1>(arg1) : std::forward<Arg2>(arg2)
在这种情况下是更合适的变体。
【讨论】:
你forward
在某些执行路径(而不是链)上两次使用相同的参数。这是一个坏主意。如果<
的右值重载具有破坏性,您将返回废话。
@Yakk 哦,我认为你是对的。我可以从你的答案中窃取这个问题的解决方案来修复我的问题吗?【参考方案4】:
您不能将临时对象绑定到非常量引用,这就是您可能会收到编译错误的原因。也就是说,在vmin(3, 2, 1, 2, 5)
中,参数是临时的。如果您将它们声明为例如int first=3,second=2
等,它将起作用,然后调用vmin(first,second...)
【讨论】:
【参考方案5】:使用 c++17 且不使用递归:
template <typename T, T ... vals>
constexpr T get_max(std::integer_sequence<T, vals...> = std::integer_sequence<T, vals...>())
T arr[sizeof...(vals)]vals...,
max = 0;
for (size_t i = 0; i != sizeof...(vals); ++i)
max = arr[i] > max ? max = arr[i] : max;
return max;
可以通过提供模板参数或整数序列作为参数来调用函数
get_max<int, 4, 8, 15, 16, 23, -42>();
using seq = std::integer_sequence<int, ...>;
get_max(seq());
【讨论】:
如果我只有类型参数并且想要类型也是结果,我如何计算最大类型。尝试这种方式,但似乎只有类型序列不起作用: templateusing seq = std::integer_sequence<Ts...>
毫无意义,并且不会在第一时间编译:您正在尝试使用可变模板参数 Ts...
实例化模板特化。但是std::integer_sequense
只接受非模板参数。查看您的代码,我猜您不需要使用decltype
,但如果您有一组类型,则需要使用std::common_type
。【参考方案6】:
C++17 中有一个解决方案胜过目前提出的所有答案:
template <typename Head0, typename Head1, typename... Tail>
constexpr auto min(Head0 &&head0, Head1 &&head1, Tail &&... tail)
if constexpr (sizeof...(tail) == 0)
return head0 < head1 ? head0 : head1;
else
return min(min(head0, head1), tail...);
注意这是怎么回事:
只需要一个函数 您不能使用少于两个参数调用它 它以最佳方式编译使用gcc
10.2 和-O3
,接受的答案编译为:
min(int, int, int):
cmp esi, edi
jge .L2
cmp esi, edx
mov eax, edx
cmovle eax, esi
ret
.L2:
cmp edi, edx
mov eax, edx
cmovle eax, edi
ret
无论出于何种原因,都有更多指令和条件跳转。 我的解决方案仅编译为:
min(int, int, int):
cmp esi, edx
mov eax, edi
cmovg esi, edx
cmp esi, edi
cmovle eax, esi
ret
这与只为三个参数递归调用std::min
相同。
(见https://godbolt.org/z/snavK5)
【讨论】:
【参考方案7】:C++20 版本
@Yakk-AdamNevraumont 提出的解决方案很好,它很好地涵盖了左值和右值的各个方面,不允许返回对临时的引用,但如果可以的话还返回左值引用。
但该解决方案现在可以针对 C++20 和 become much more concise and elegant 进行现代化改造:
template<typename... Ts>
struct common_return
using type = std::common_reference_t<Ts...>;
;
template<typename T, typename... Ts>
requires std::is_lvalue_reference_v<T> &&
(std::is_lvalue_reference_v<Ts> && ...)
&& ( std::same_as<T, Ts> && ... )
struct common_return<T, Ts...>
using type = std::common_reference_t<T, Ts...>&;
;
template<typename... Ts>
using common_return_t = typename common_return<Ts...>::type;
namespace my_min
template<typename T>
T min(T&& t)
return std::forward<T>(t);
template<typename T1, typename T2, typename... Ts>
common_return_t<T1, T2, Ts...> min(T1&& t1, T2&& t2, Ts&&... ts)
if(t2 > t1)
return min(std::forward<T1>(t1), std::forward<Ts>(ts)...);
return min(std::forward<T2>(t2), std::forward<Ts>(ts)...);
【讨论】:
以上是关于实现可变参数 min / max 函数的主要内容,如果未能解决你的问题,请参考以下文章