Улучшение функции шаблона переменных с использованием новых функций c 14 / c 17

#c #c 14 #c 17 #variadic-templates #fold-expression

#c #c 14 #c 17 #variadic-шаблоны #fold-выражение

Вопрос:

Я новичок в шаблонах переменных, но мне все же удалось запрограммировать некоторый код на c 11, используя его, но я все еще чувствую недовольство результатом, потому что ему не хватает выразительности.

Проблема заключается в реализации функции, которая принимает несколько bool условий (от 1 до любого) и возвращает целочисленный код, указывающий, в каком месте находится первый false аргумент «», или 0 если все они есть true .

 e.g. "error_code(true, true, true);" must return 0
e.g. "error_code(true, true, false);" must return 3
e.g. "error_code(true, false, false);" must return 2
e.g. "error_code(false, false, false);" must return 1
 

Мой текущий код расшифровывается как (прямая ссылка на coliru: http://coliru.stacked-crooked.com/a/1b557f2819ae9775 ):

 #include <tuple>
#include <iostream>

int error_impl(bool cond)
{
    return cond;
}

template<typename... Args>
int error_impl(bool cond, Args... args)
{
    return cond ? 1   error_impl(args...) : 0;
}

template<typename... Args>
int error_code(Args... args)
{
    constexpr std::size_t size = std::tuple_size<std::tuple<Args...>>::value   1;
    return (error_impl(args...)   1) % size;
}

int main()
{
    auto e1 = error_code(true, true, true);
    auto e2 = error_code(true, true, false);
    auto e3 = error_code(true, false, false);
    auto e4 = error_code(false, false, false);
    std::cout << std::boolalpha;
    std::cout << "(true, true, true)==0 -> " << (e1 == 0) << "n";
    std::cout << "(true, true, false)==3 -> " << (e2 == 3) << "n";
    std::cout << "(true, false, false)==2 -> " << (e3 == 2)<< "n";
    std::cout << "(false, false, false)==1 -> " << (e4 == 1)<< "n";

    auto e5 = error_code(true, true, true, true);
    auto e6 = error_code(true, true, true, false);
    auto e7 = error_code(true, true, false, false);
    auto e8 = error_code(true, false, false, false);
    auto e9 = error_code(false, false, false, false);
    std::cout << "(true, true, true, true)==0 -> " << (e5 == 0) << "n";
    std::cout << "(true, true, true, false)==4 -> " << (e6 == 4) << "n";
    std::cout << "(true, true, false, false)==3 -> " << (e7 == 3) << "n";
    std::cout << "(true, false, false, false)==2 -> " << (e8 == 2)<< "n";
    std::cout << "(false, false, false, false)==1 -> " << (e9 == 1)<< "n";
}
 

Интересно, где эту error_code() функцию «» можно улучшить, используя новые функции развертывания из c 14 / c 17, чтобы она улучшала выразительность и использовала менее 3 функций.

Любая помощь будет признательна!

Ответ №1:

C 17 со сворачиванием:

 template<class... Bools>
constexpr unsigned error_code(Bools... Bs) {
    unsigned rv = 1;
    (void) ((rv  = Bs, !Bs) || ...);
    return rv % (sizeof...(Bs)   1);
}
 

Незапрошенный, так что это просто бонус — та же идея, C 20:

 constexpr unsigned error_code(auto... Bs) {
    unsigned rv = 1;
    (void) ((rv  = Bs, !Bs) || ...);
    return rv % (sizeof...(Bs)   1);
}
 

Объяснение:

  • Первая часть выражения fold содержит две части , разделенные символом a , . Результат левой части отбрасывается, и результатом такого выражения является крайняя правая часть, !Bs .
     (rv  = Bs, !Bs)
     
  • Вторая часть || ... — это то, где происходит складывание (или раскладывание). Первое выражение копируется / вставляется повторно до тех пор, пока в пакете не останется больше аргументов. Ибо true, false, true это становится:
     (rv  = 1, !true) || (rv  = 0, !false) || (rv  = 1, !true)
     

    или

     (rv  = 1, false) || (rv  = 0, true) || (rv  = 1, false)
     
  • Начинается оценка короткого замыкания. Когда встроенный оператор 1 || имеет a true с левой стороны, правая сторона не вычисляется. Вот почему в этом примере выполняется только один из rv = 1 «s». (rv = 0, true) Останавливает оценку, поэтому оценивается только это:
     (rv  = 1, false) || (rv  = 0, true)
     
  • Последнее rv % (sizeof...(Bs) 1); — позаботиться о случае , когда false значения не найдены и мы должны вернуться 0 . Пример:
     unsigned rv = 1;
    (rv  = 1, !true) || (rv  = 1, !true) || (rv  = 1, !true);
    
    // rv is now 4, and sizeof...(Bs) == 3, so:
    
    4 % (3   1) == 0
     
  • Почему (void) ?
    Компиляторы любят предупреждать о том, что они считают неиспользуемыми выражениями. Тщательно размещенный (void) символ говорит ему, что нам все равно, поэтому он заставляет компилятор молчать.

1 — Это не относится к определяемым пользователем операторам.

Комментарии:

1. извините, я по ошибке удалил свой последний комментарий. Я пишу это снова: большое вам спасибо за ваш ответ! Мне потребуется некоторое время, чтобы по-настоящему разобраться в коде, но теперь у меня есть четкий код, чтобы продолжать совершенствовать свои (пока немногочисленные) навыки работы с вариационными шаблонами. Кроме того, я хотел бы поблагодарить вас за ваш раздел c 20. Как разработчик, я с нетерпением жду начала работы с c 20 и его новыми функциями, особенно диапазонами, поэтому приветствуется любое новое обучение.

2. Кстати, я не знаю конструкцию «sizeof … (Bs)», которая позволяет избавиться от ужасного взлома кортежей, который я использовал для вычисления количества аргументов в функции.

3. @Пабло sizeof... действительно хороший! Я редко использую его в повседневной жизни 🙂

4. @max66 Спасибо тебе! Мне твой тоже понравился! Мне потребовалось достаточно времени, чтобы создать решение на C 17, и я слишком устал, чтобы перезагрузить свой мозг, чтобы создать версию на C 14, как это сделали вы. Тем не менее, я застрял с C 14 на работе 🙂

5. @TedLyngmo — Вы правы; извините: я только перенес выражение сгиба. Забудь об этом.

Ответ №2:

Как насчет (C 17) следующим образом?

 template <typename... Args>
int error_code (Args... args)
 {
   int ret = 0;
   int val = 1;

   ( (args || ret ? 0 : ret = val,   val), ... );

   return ret;
 }
 

В C 11 и C 14 требуется немного (немного!) больше машинописи.

 template <typename... Args>
int error_code (Args... args)
 {
   using unused = int[];

   int ret = 0;
   int val = 1;

   (void)unused{ 0, (args || ret ? 0 : ret = val,   val)... };

   return ret;
 }
 

Комментарии:

1. Большое вам спасибо за ваш ответ! Мне потребуется некоторое время и целый сеанс отладчика, чтобы понять, как это работает, но, по крайней мере, теперь у меня есть надежный код, позволяющий углубляться в шаблоны переменных.

Ответ №3:

Поскольку вы знаете, что все ваши аргументы будут преобразованы в a bool , лучше вообще не использовать переменные аргументы:

 inline int error_code(std::initializer_list<bool> args) {
    int index = std::find(args.begin(), args.end(), false) - args.begin();
    if (index == args.size()) return 0;
    return 1   index;
}

// Either directly call the above `error_code({ true, true, false, ... })`
// Or if you must have a parameter pack

template<typename... Args>
int error_code(Args... args) {
    std::initializer_list<bool> v{ args... };
    int index = std::find(v.begin(), v.end(), false) - v.begin();
    if (index == sizeof...(args)) return 0;
    return index   1;
    // If you have both functions, simply have: return error_code({ args... });
}
 

Компилятор, похоже, оптимизирует его аналогично вашему вариантному решению (и оно даже работает в C 11).


Вот более интересное решение, которое использует C 17-кратные выражения:

 template<typename... Args, int... I>
int error_code_impl(Args... args, std::integer_sequence<int, I...>) {
    int result = 0;
    ([amp;result](bool arg){
        if (!arg) result = I   1;
        return arg;
    }(args) amp;amp; ...);
    // Can also be written without the lambda as something like:
    // ((args ? true : ((result = I   1), false)) amp;amp; ...);
    return resu<  
}

template<typename... Args>
int error_code(Args... args) {
    std::make_integer_sequence<int, sizeof...(args)> indices;
    return error_code_impl<Args...>(indices, args...);
}
 

Комментарии:

1. Действительно круто! На первый взгляд, я почти поддался искушению использовать переменные функции из старого доброго простого языка C. Ваше решение запомнило меня, но ваше решение улучшается с использованием «std::initializer_list» вместо ключевых слов va_start, va_end … жутко. Мне очень нравится ваше первое решение, до сих пор оно было наилучшим для чтения, и его легко понять любому