实现可变参数 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,我会得到以下结果

    通用引用&amp;&amp; -> 编译错误 const 引用 const&amp; -> 好的 普通引用&amp; -> 编译错误

    现在我知道了function templates mix weirdly with templates,但是手头的混合有什么具体的诀窍吗? 我应该选择哪种类型的论据?

    参数包的扩展不够吗?我真的需要将我的论点转发给递归调用吗?

    当包装在结构中并作为静态成员函数公开时,此功能是否可以更好地实现。部分专业化的能力能给我带来什么吗?

    功能版本是否有更健壮/高效的实现/设计? (特别是我想知道constexpr 版本是否与模板元编程的效率相匹配)

【问题讨论】:

好问题。你甚至可以像this 一样实现min 为什么不使用std::minstd::initializer_list,即template &lt;typename ...Args&gt; auto vmin(Args&amp;&amp;... args) return std::min( std::forward&lt;Args&gt;(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&lt;b)?a:b) = 7 是合法的 C++,但 vmin( a, b ) = 7 不是,因为 std::common_type decays 是盲目的参数(由于我认为它在输入两个值时返回右值引用的过度反应 -在 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&amp;)&amp;&amp;=delete - 即,它们不会阻止分配右值,如果min 中的一种类型不支持,这可能会导致意外.基本类型可以。

附注:开始删除您的右值引用operator=s 人。

【讨论】:

嗯。尽管对common_type 的天真阅读说common_type&lt;X,Y&gt;::typetrue?std::declval&lt;X&gt;():std::declval&lt;Y&gt;() 的类型,但似乎common_type&lt;int&amp;, int&amp;&gt;::type 不是int&amp; 而是int。奇怪。 我认为基本情况应该有一个参数,这也消除了将T2&amp;&amp; v0 与参数包分开的需要。见ideone.com/5J6BBw 哦,更好的是,完全失去std::common_type。见ideone.com/OcQZ06 @Yakk 我认为是因为common_type&lt;int&amp;, float&amp;&gt;::type 不能是float&amp; Stepanov 认为min(a, b) 应该返回a iff a &lt;= b,即b &lt; 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:一个问题,在这种情况下&amp;&amp;const &amp; 相比有什么优势,而min 不复制或修改输入?像this一样通过const &amp;(不需要完美转发)传递输入不是更好吗? @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&lt;Arg1&gt;(arg1) &lt; std::forward&lt;Arg2&gt;(arg2) ? std::forward&lt;Arg1&gt;(arg1) : std::forward&lt;Arg2&gt;(arg2) 在某些情况下可能会导致问题。 arg1 &lt; arg2 ? std::forward&lt;Arg1&gt;(arg1) : std::forward&lt;Arg2&gt;(arg2) 在这种情况下是更合适的变体。

【讨论】:

forward 在某些执行路径(而不是链)上两次使用相同的参数。这是一个坏主意。如果&lt; 的右值重载具有破坏性,您将返回废话。 @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());

【讨论】:

如果我只有类型参数并且想要类型也是结果,我如何计算最大类型。尝试这种方式,但似乎只有类型序列不起作用: template struct max using seq = std::integer_sequence;使用 type = decltype( get_max( seq() ) ); ; @SergeyInozemcev using seq = std::integer_sequence&lt;Ts...&gt; 毫无意义,并且不会在第一时间编译:您正在尝试使用可变模板参数 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 函数的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Julia 中为可变结构设置默认参数?

C语言中如何实现可变参函数

可变参数模板类:是不是可以为每个可变参数模板参数实现一个唯一的成员函数?

C语言奇淫技巧之函数的可变参数

怎么将可变参数的函数的参数传递给另一个可变参数的函数

亲密接触C可变参数函数