Определите эффективный тип литерала при сравнении

#c #templates #macros #compiler-warnings

#c #шаблоны #макросы #предупреждения компилятора

Вопрос:

Ниже приведена упрощенная версия макроса, который я определил:

 #define CHECK_EQ(a, b) do { if ((a) != (b)) abort(); } while (false)
  

который работает, но теперь я хотел бы выполнить дополнительную работу с оцененными значениями a / b и хотел бы оценить каждое значение только один раз. Другими словами, что-то вроде:

 #define CHECK_EQ(a, b)          
  do {                          
    const auto a_eval = (a);    
    const auto b_eval = (b);    
    if (a_eval != b_eval) {     
      /* Print a_eval/b_eval */ 
      abort();                  
    }                           
  } while (false)
  

но это нарушает некоторые текущие способы использования, вызывая, -Wsign-compare например CHECK_EQ(some_unsigned, 1) . Вместо auto определения типа, в который будет преобразована каждая сторона сравнения для сравнения, я бы хотел. Гипотетический пример:

 #define CHECK_EQ(a, b)                                           
  do {                                                           
    using CmpType = CommonType<decltype(a), decltype(b)>::type;   What goes here??
    const CmpType a_eval = (a);                                  
    const CmpType b_eval = (b);                                  
    if (a_eval != b_eval) {                                      
      /* Print a_eval amp; b_eval */                                
      abort();                                                   
    }                                                            
  } while (false)
  

Я подозреваю, что это тоже не совсем правильно, поскольку decltype(1) будет int . Есть ли какой-либо способ выполнить то, что я хотел бы, без изменения существующих CHECK_EQ вызовов или подавления предупреждения?

Редактировать: Кажется, есть небольшая путаница в том, что должно и не должно возвращать предупреждение. Использование auto возвращает ненужное предупреждение, когда один из аргументов является положительным литералом, который также является допустимым литералом без знака (но auto приводит к signed ). Другими словами, в идеале CHECK_EQ(a, b) было бы выдавать предупреждение тогда и только тогда, a == b если бы. Вторым лучшим решением было бы разрешить смешивание типов при условии, что в конечном итоге выполняемое сравнение безопасно с точки зрения подписи типов. Похоже, это достигается с помощью std::common_type .

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

1. Вы ищете std::common_type ? Или, может быть decltype( a b ) ?

2. Предупреждение кажется мне действительным в этой ситуации (то есть это предупреждение о законной потенциальной проблеме с вызывающим кодом). Вы должны использовать соответствующее приведение, чтобы гарантировать сопоставимость выражений a и b при вызове макроса, а не внутри самого макроса.

Ответ №1:

(РЕДАКТИРОВАТЬ в конце есть альтернативное решение)

Решение 1 (оригинальное)

Это никогда не работало корректно и будет неверным как с CommonType, так и с std:::common_type . Это было и будет неверно, поскольку ~(0U) != -1 вычисляется как false в такой схеме (предполагающей дополнение 2), где вы, похоже, ожидаете, что оно вернется true .

Я бы предложил использовать шаблонные функции:

 // check if this is a simple int literal 
// such as 1, 0, 6789, but not 1U and neither expressions like -1.
template <class T1, class T2>
bool is_same(const T1amp; a, const T2amp;b)
{
   if (std::is_signed_v<T1> amp;amp; !std::is_signed_v<T2>) {
       // some compilers might warn about the following,
       // in that case make it an "if constexpr" instead.
       if (a < 0) return false;
   }
   if (!std::is_signed_v<T1> amp;amp; std::is_signed_v<T2>) {
       if (b < 0) return false;
   }
   std::common_type_t<T1, T2> a_common = a;
   std::common_type_t<T1, T2> b_common = b;
   return a == b;
}
  

Затем вы можете написать:

 #define CHECK_EQ(a, b)                   
  do {                                   
    if (!is_same(a_eval, b_eval)) {      
      /* Print a_eval amp; b_eval */        
      abort();                           
    }                                    
  } while (false)
  

Но если уж мы на этом остановились, почему бы просто не использовать шаблонные функции полностью?

 template <typename T, typename U>
void check_eq(const Tamp; a, const Uamp; b)
{
   if (!is_same(a,b))
   {
       /* print a and b */
       abort();
   }
}
  

Примечание: Если у вас C 14, а не C 17, то замените std::is_signed_v<T> на std::is_signed<T>::value . Если у вас C 11 и даже не C 14, то замените std::common_type_t<T1, T2> на typename std::common_type<T1, T2>::type .


Решение 2

После редактирования вопроса кажется, что существует различие между литералом int и любым другим типом int значения. Код должен выдавать то же предупреждение, что и для, a == b where a == 1 не будет предупреждать, если a значение без знака.

Для этого я представляю макрос IS_INT_LITERAL:

 template <std::size_t N>
constexpr bool is_int_str(const char (amp;str)[N])
{
    // TODO: deal with 0x1Dbef hex literals
    if (N < 2 || str[N-1] != '') return false;
    for (unsigned i=0 ; i < N-1 ;   i)
        // NOTE: This is only 99.9% portable. It assumes that '0'..'9' chars are consecutive.
        //A more portable way would check (str[i] != '0 amp;amp; str[i] != '1' ...)
        if (str[i] < '0' || str[i] > '9') {
            if (i == 0) return false;
            // support 2ull , 1L, etc.
            if (str[i] !='U' amp;amp; 
                 str[i] != 'L' amp;amp;
                 str[i] != 'u' amp;amp;     
                 str[i] != 'l' ) /* lower case L*/
            {
                return false;
            }
        }
    return true;
}
#define IS_INT_LITERAL(x) is_int_str(#x)
  

Затем макрос можно использовать в функции сравнения:

 template <bool suppress_sign_warnings, class T1, class T2>
bool is_same(const T1 amp; a, const T2 amp; b)
{
    if constexpr (suppress_sign_warnings) {
        std::common_type_t<T1, T2> a_common = a, b_common = b;
        return a_common == b_common;
    } else {
        return a == b;
    }
}

#define CHECK_EQ(a, b)          
  do {                          
    const auto a_eval = (a);    
    const auto b_eval = (b);    
    constexpr bool any_literal = IS_INT_LITERAL(a) || IS_INT_LITERAL(b); 
    if (! is_same<any_literal>(a_eval, b_eval)) {     
      /* Print a_eval/b_eval */ 
      abort();                  
    }                           
  } while (false)
  

Это работает без предупреждений:

 CHECK_EQ(1, 1u); // like 1 == 1u
  

Но при этом выдается предупреждение:

 void foo(int a, unsigned b = 1u)
{
   CHECK_EQ(a, b); // like a == b
}
  

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

1. Извините, откуда берется ~ (0u) и почему вы говорите, что я ожидаю, что он будет отличаться от -1? Кроме того, я не был знаком с common_type , я думаю, это все, что мне нужно; какова цель выполнения проверок is_signed вместо того, чтобы просто переходить к части common_type_t?

2. @Luis вопрос имеет CHECK_EQ(some_unsigned, 1) . 0u — это значение без знака, равное 0. Таким образом, ~(0u) это максимальное значение без знака. Значение без знака является положительным и математически отличается от всех отрицательных значений, таких как -1 . Компилятор предупреждает, что язык производит сравнение, которое математически неверно, и это то, чего я пытаюсь избежать в ответе. Ответ пытается поддерживать математическую корректность, как это автоматически делают другие языки.

3. @Luis Я обновил ответ в соответствии с вашей первой альтернативой — по-другому обрабатываю литералы int и подавляю предупреждения только для литералов

4. Спасибо @Michael, я думаю, что выполнить точный эквивалент буквального a == b выражения не так просто

Ответ №2:

Может быть, использовать шаблонную функцию для сравнения?

 #include <iostream>

template<typename T1, typename T2>
static inline bool _NotEqual(const T1amp; a, const T2amp; b)
{
  if (static_cast<T2>(static_cast<T1>(b)) == b) {
    return a != static_cast<T1>(b);
  } else {
    return static_cast<T2>(a) != b;
  }
}

#define CHECK_EQ(a, b)                                          
  do {                                                          
    const auto a_eval = (a);                                    
    const auto b_eval = (b);                                    
    if (_NotEqual(a_eval, b_eval)) {                            
      std::cerr << a_eval <<" != "<< b_eval << std::endl;       
      abort();                                                  
    }                                                           
  } while (false)

int main()
{
  CHECK_EQ(1U, 1);
  CHECK_EQ(2, 2.2);
}
  

Предполагая, что T1 и T2 могут статически приводить друг к другу.

Редактировать:

Что касается опасения, что ~(0U) == -1 если бы это было нежелательно, то нам, вероятно, не следовало пытаться отменить предупреждение компилятора в первую очередь. Но ~(0U) == -1 на самом деле это не так уж плохо, например, существует довольно много случаев, когда стандартная библиотека использует «-1» для возврата без знака.

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

1. Зачем продолжать использовать макрос? По мере того, как макрос становится длиннее, целесообразность использования макроса уменьшается.

2. Я просто следую примеру оригинального poster, пытаясь внести минимальные изменения в его исходный код.