Внедрение вариационных функций min/max

Я реализую вариационные функции 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;
}

Теперь работает, но у меня есть несколько вопросов/проблем:

  • невариантная перегрузка должна принимать свои аргументы по значению. Если я попробую передать другие типы ссылок, я получаю следующие результаты:

    • универсальные ссылки && → ошибка компиляции
    • const ссылки const& → OK
    • простые ссылки & → ошибка компиляции

    Теперь я знаю, что шаблоны функций смешались с шаблонами, но есть ли какие-либо конкретные ноу-хау для этого микса? Какой тип аргументов я должен выбрать?

  • Не будет ли расширение пакета параметров достаточным? Действительно ли мне нужно перенаправить свои аргументы на рекурсивный вызов?

  • Является ли эта функция лучше реализована, когда она завершена внутри структуры и отображается как статическая функция-член. Будет ли возможность частичной специализации купить мне что-нибудь?

  • Есть ли более эффективная/эффективная реализация/дизайн для версии функции? (в частности, мне интересно, будет ли версия constexpr соответствовать эффективности метапрограммирования шаблона)

Ответ 1

живой пример

Это отличная пересылка по аргументам. Он полагается на RVO для возвращаемых значений, так как он возвращает тип значения независимо от типов ввода, потому что common_type делает это.

Я реализовал вывод common_type, позволяющий передавать смешанные типы и выводить результат "ожидаемого" результата.

Мы поддерживаем min из 1 элемента, потому что он делает slicker кода.

#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 является законным С++, но vmin( a, b ) = 7 не является, потому что std::common_type decay является аргументами вслепую (вызвано тем, что я считаю чрезмерной реакцией на его возвращение ссылок rvalue при подаче двух типов значений в более старой версии std::common_type).

Просто использование decltype( true?a:b ) заманчиво, но оно приводит к задаче справки rvalue и не поддерживает common_type специализации (в качестве примера, std::chrono). Поэтому мы оба хотим использовать common_type и не хотим его использовать.

Во-вторых, запись функции min, которая не поддерживает несвязанные указатели и не позволяет пользователю изменять функцию сравнения, кажется неправильной.

Таким образом, следующая более сложная версия выше. живой пример:

#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 - то есть, они не блокируют присвоение значений r, что может привести к неожиданностям, если один из типов в min не, Основные типы.

Какая сторона примечания: начните удалять ссылку rvalue operator= людей.

Ответ 2

Я ценю мысль, которую Якк вводил в обратные типы, поэтому мне это не нужно, но это становится намного проще:

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)...);
}

Вывод типа возвращаемого типа является довольно устрашающим (может потребоваться С++ 14).

Ответ 3

Вы не можете привязать временную ссылку к неконстантной ссылке, поэтому вы, вероятно, получите ошибку компиляции. То есть, в vmin(3, 2, 1, 2, 5), параметры являются временными. Он будет работать, если вы объявите их как, например, int first=3,second=2 и т.д., Затем вызовите vmin(first,second...)

Ответ 4

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;
}

Смотрите живой пример.

Изменить: Как отмечалось в комментариях @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) является более подходящим вариантом в этом случае.