Перегрузка функции, основанная на возможности вызывать оператор вывода потока для своих аргументов - PullRequest
0 голосов
/ 24 мая 2019

У меня есть универсальная функция, которая принимает два аргумента, сравнивает их и печатает сообщение, если они не равны.Прямо сейчас, у меня просто эта относительно тупая функция:

template <typename T>
static void AreEqual(const T& expected,
                     const T& actual,
                     const std::string& message = "") {
    if (!(actual == expected)) {
        std::cout << message;
    }
}

Это работало адекватно в течение многих лет.Он чаще всего вызывается с примитивами, но также используется для сравнения больших пользовательских структур / классов.

Я хотел бы расширить функцию, предоставив перегрузку, которая печатает ожидаемые и фактические значения, когда они несоответствовать, но не нарушая функцию для классов, которые определяют operator==, но не определяют operator<<.Моя идея состоит в том, чтобы создать перегрузку, которая использует SFINAE, чтобы отключить перегрузку, если отсутствует operator<<.Я до сих пор придумал это:

template <
    typename T,
    typename = typename std::enable_if_t<
        std::is_same_v<decltype(std::cout << *((T*)nullptr)), decltype(std::cout)>>>
static void AreEqual(const T& expected,
                     const T& actual,
                     const std::string& message = "") {
    if (!(actual == expected)) {
        std::cout << "Expected " << expected << ", got " << actual << ". " << message;
    }
}

Это компилируется, но не выбирается для T из int или std::string, и я не уверен почему.Мое первое подозрение состоит в том, что мои аргументы is_same_v как-то искажены, но я понятия не имею, как или как выяснить, как это исправить, если это так.

Вопрос 1: Все ли это даже необходимо?Могу ли я добиться того же результата без метапрограммирования шаблонов (желательно с использованием C ++ 11)

Вопрос 2: Если это лучший путь вперед, как эффективно отлаживать свои шаблоны?

Ответы [ 2 ]

1 голос
/ 24 мая 2019

Вы можете сделать что-то вроде:

struct overload_low_priority {};
struct overload_high_priority : overload_low_priority {};


template <typename T>
static auto AreEqualImpl(const T& expected,
                         const T& actual,
                         const std::string& message,
                         overload_high_priority)
-> decltype(std::cout << expected, void()) // SFINAE
{
    if (!(actual == expected)) {
        std::cout << "Expected " << expected << ", got " << actual << ". " << message;
    }
}

template <typename T>
static void AreEqualImpl(const T& expected,
                         const T& actual,
                         const std::string& message,
                         overload_low_priority) // Fallback
{
    if (!(actual == expected)) {
        std::cout << message;
    }
}

template <typename T>
static void AreEqual(const T& expected,
                     const T& actual,
                     const std::string& message = "")
{
    AreEqualImpl(expected, actual, message, overload_high_priority{});
}
0 голосов
/ 24 мая 2019

Я предлагаю что-то другое.

Вместо std::cout (или нет) значений expected и actual вы можете вывести значение, возвращаемое функцией, вызванной из значений. Таким образом, вы можете различить (перегрузить) поведение вызываемой функции.

Я имею в виду ... предположим, вы пишете две версии функции maybePrint().

Первая - это сквозная функция шаблона, SFINAE включается только в том случае, если тип шаблона является печатаемым

template <typename T>
auto maybePrint (T const & t) -> decltype( std::cout << t, t )
 { return t; }

Второй, вызываемый, когда первый недоступен (поэтому, когда аргумент недоступен для печати), возвращает информативную строку (ну ... может быть, выбрать лучшую строку) [РЕДАКТИРОВАТЬ: изменено после отчета из Jarod42]

template <typename ... Ts>
std::string maybePrint (Ts const & ...)
 { return "[maybe not]"; }

Итак, ваш AreEqual() станет

template <typename T>
static void AreEqual(const T& expected,
                     const T& actual,
                     const std::string& message = "")
 {
   if ( ! (actual == expected) )
      std::cout << "Expected " << maybePrint(expected) << ", got "
         << maybePrint(actual) << ". " << message;
 }

Я предлагаю это решение также потому, что завтра или в далеком будущем у вас может возникнуть соблазн изменить AreEqual() для дифференциации типов шаблонов.

Это потому что следующий звонок

AreEqual(1, 2l, "abc\n");

выдает ошибку компиляции, поскольку компилятор не может выбирать между T = int (1 - int) и T = long (2l - long).

Если вы переписываете AreEqual(), получая два аргумента (потенциально) двух разных типов

template <typename T1, typename T2>
static void AreEqual (T1 const & expected,
                      T2 const & actual,
                      std::string const & message = "")
 {
   if ( ! (actual == expected) )
      std::cout << "Expected " << maybePrint(expected) << ", got "
         << maybePrint(actual) << ". " << message;
 }

предыдущий вызов компилируется, потому что T1 выводится int и T2 выводится long.

Если вы включаете одну или другую версию AreEqual() в соответствии с T1 и T2, у вас есть (потенциально) четыре случая (T1 и T2 для печати; T1 для печати, T2 нет; T2 для печати, T1 не для печати; T1 и T2 не для печати), поэтому четыре версии AreEqual().

Работая через maybePrint(), вы поддерживаете один AreEqual().

...