我正在实现可变参数min / max函数。目标是利用编译时已知的参数数量并执行展开的评估(避免运行时循环)。代码的当前状态如下(呈现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,我会得到以下结果
&&
- &gt;编译错误const&
- &gt;行&
- &gt;编译错误现在我知道function templates mix weirdly with templates但是有没有具体的技术诀窍呢? 我应该选择哪种类型的参数?
参数包的扩展是否足够?我是否真的需要将我的参数转发给递归调用?
当在struct中包装并作为静态成员函数公开时,这个函数是否更好实现。部分专业的能力会给我带来什么吗?
功能版本是否有更强大/更有效的实现/设计? (特别是我想知道constexpr
版本是否与模板元编程的效率相匹配)
答案 0 :(得分:22)
这样可以完美地转发参数。它依赖于RVO来返回值,因为无论输入类型如何,它都返回一个值类型,因为common_type
会这样做。
我实施了common_type
扣除,允许传入混合类型,以及“预期”结果类型输出。
我们支持1个元素的min,因为它使代码更加流畅。
#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
是盲目的论据(由我认为是对它的过度反应造成的)在较早的std::common_type
实现中提供两个值类型时返回rvalue引用。
简单地使用decltype( true?a:b )
很有诱惑力,但它都会导致rvalue引用问题,并且不支持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
- 也就是说,它们不会阻止rvalues被分配,如果{{1}中的一个类型可能会导致意外}} 才不是 。基本类型。
这是旁注:开始删除您的左值参考min
人。
答案 1 :(得分:15)
我很欣赏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)。
答案 2 :(得分:3)
您不能将临时绑定到非const引用,这就是您可能收到编译错误的原因。也就是说,在vmin(3, 2, 1, 2, 5)
中,参数是临时值。如果您将它们声明为例如int first=3,second=2
等等,那么它将起作用,然后调用vmin(first,second...)
答案 3 :(得分:3)
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)
答案 4 :(得分:2)
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 时,代码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)
更合适。
答案 5 :(得分:0)
在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());