Были ли когда-нибудь изменения в тихом поведении C ++ с новыми стандартными версиями? - PullRequest
104 голосов
/ 06 августа 2020

(Я ищу пример или два, чтобы доказать это, а не список.)

Было ли когда-либо изменение стандарта C ++ (например, с 98 на 11, 11 to 14 et c.) изменил поведение существующего, правильно сформированного пользовательского кода с определенным поведением - незаметно? т.е. без предупреждений или ошибок при компиляции с более новой стандартной версией?

Примечания:

  • Я спрашиваю о поведении в соответствии со стандартами, а не о выборе разработчика / автора компилятора.
  • Чем менее надуманный код, тем лучше (как ответ на этот вопрос).
  • Я не имею в виду код с определением версии, такой как #if __cplusplus >= 201103L.
  • Ответы с использованием модели памяти в порядке.

Ответы [ 9 ]

112 голосов
/ 06 августа 2020

Тип возвращаемого значения string::data изменяется с const char* на char* в C ++ 17. Это, безусловно, может иметь значение

void func(char* data)
{
    cout << data << " is not const\n";
}

void func(const char* data)
{
    cout << data << " is const\n";
}

int main()
{
    string s = "xyz";
    func(s.data());
}

Немного надумано, но эта легальная программа изменит свой вывод. с C ++ 14 на C ++ 17.

82 голосов
/ 07 августа 2020

Ответ на этот вопрос показывает, как инициализация вектора с использованием одного значения size_type может привести к различному поведению между C ++ 03 и C ++ 11.

std::vector<Something> s(10);

C ++ 03 по умолчанию создает временный объект типа элемента Something и копирует каждый элемент в векторе из этого временного объекта.

C ++ 11 по умолчанию создает каждый элемент в векторе.

Во многих (в большинстве?) Случаев это приводит к эквивалентному конечному состоянию, но нет причин, по которым они должны это делать. Это зависит от реализации конструкторов по умолчанию / копирования Something.

См. этот надуманный пример :

class Something {
private:
    static int counter;

public:
    Something() : v(counter++) {
        std::cout << "default " << v << '\n';
    }

    Something(Something const & other) : v(counter++) {
        std::cout << "copy " << other.v << " to " << v << '\n';
    }

    ~Something() {
        std::cout << "dtor " << v << '\n';
    }

private:
    int v;
};

int Something::counter = 0;

C ++ 03 будет строить по умолчанию один Something с v == 0, затем скопируйте-сконструируйте еще десять из этого. В конце вектор содержит десять объектов, v значения которых от 1 до 10 включительно.

C ++ 11 будет создавать каждый элемент по умолчанию. Копии не делаются. В конце вектор содержит десять объектов, v значения которых от 0 до 9 включительно.

50 голосов
/ 07 августа 2020

В стандарте есть список критических изменений Приложение C [diff] . Многие из этих изменений могут привести к изменению бесшумного поведения.

Пример:

int f(const char*); // #1
int f(bool);        // #2

int x = f(u8"foo"); // until C++20: calls #1; since C++20: calls #2
25 голосов
/ 07 августа 2020

Это происходит каждый раз, когда они добавляют новые методы (и часто функции) в стандартную библиотеку.

Предположим, у вас есть стандартный тип библиотеки:

struct example {
  void do_stuff() const;
};

довольно просто. В некоторой стандартной версии добавляется новый метод, перегрузка или что-то еще:

struct example {
  void do_stuff() const;
  void method(); // a new method
};

это может незаметно изменять поведение существующих программ C ++.

Это связано с тем, что в настоящее время отражение в C ++ ограничено возможностей достаточно, чтобы обнаружить , если такой метод существует, и запустить на его основе другой код.

template<class T, class=void>
struct detect_new_method : std::false_type {};

template<class T>
struct detect_new_method< T, std::void_t< decltype( &T::method ) > > : std::true_type {};

это просто относительно простой способ обнаружить новый method, там существует множество способов.

void task( std::false_type ) {
  std::cout << "old code";
};
void task( std::true_type ) {
  std::cout << "new code";
};

int main() {
  task( detect_new_method<example>{} );
}

То же самое может произойти, когда вы удаляете методы из классов.

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

Стандарт добавляет к контейнеру метод .data(), и внезапно тип меняет путь, который он использует для сериализации.

Все, что может делать стандарт C ++, если он не хочет зависать, это сделать код, который молча ломается, редким или каким-то образом неразумным.

15 голосов
/ 11 августа 2020

Вот пример, который печатает 3 в C ++ 03 и 0 в C ++ 11:

template<int I> struct X   { static int const c = 2; };
template<> struct X<0>     { typedef int c; };
template<class T> struct Y { static int const c = 3; };
static int const c = 4;
int main() { std::cout << (Y<X< 1>>::c >::c>::c) << '\n'; }

Это изменение поведения было вызвано специальной обработкой для >>. До C ++ 11 >> всегда был оператором сдвига вправо. В C ++ 11 >> также может быть частью объявления шаблона.

15 голосов
/ 07 августа 2020

О, мальчик ... Ссылка cpplearner предоставлено это страшно .

Среди прочего, C + +20 запрещенных C -стиль-объявление структуры структур C ++.

typedef struct
{
  void member_foo(); // Ill-formed since C++20
} m_struct;

Если вас учили писать такие структуры (и люди, которые учат "C с классами" учат именно этому), вы на винтах .

11 голосов
/ 08 августа 2020

Триграфы отброшены

Исходные файлы закодированы в физическом наборе символов , который в зависимости от реализации сопоставляется с исходным набором символов , который определен в стандарте. Чтобы приспособить сопоставления из некоторых физических наборов символов, которые изначально не имели всех знаков препинания, необходимых для исходного набора символов, язык определил триграфы - последовательности из трех общих символов, которые можно было использовать вместо менее распространенного символа пунктуации. Для их обработки требовались препроцессор и компилятор.

В C ++ 17 триграфы были удалены. Таким образом, некоторые исходные файлы не будут приняты более новыми компиляторами, если они не будут сначала переведены из физического набора символов в какой-либо другой физический набор символов, который однозначно отображает исходный набор символов. (На практике большинство компиляторов просто сделали интерпретацию триграфов необязательной.) Это не тонкое изменение поведения, а критическое изменение, предотвращающее компиляцию ранее приемлемых исходных файлов без внешнего процесса перевода.

Подробнее ограничения на char

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

Стандарт C ++ определяет char как целочисленный тип, возможно, без знака, который может эффективно представлять каждое значение в наборе символов выполнения. Используя представление языкового юриста, вы можете утверждать, что char должно быть не менее 8 бит.

Если ваша реализация использует беззнаковое значение для char, то вы знаете, что оно может быть в диапазоне от 0 до 255 и, таким образом, подходит для хранения всех возможных байтовых значений.

Но если ваша реализация использует значение со знаком, у нее есть параметры.

Большинство будет использовать два дополнения, давая char a минимальный диапазон от -128 до 127. Это 256 уникальных значений.

Но другой вариант - знак + величина, где один бит зарезервирован, чтобы указать, является ли число отрицательным, а остальные семь битов указывают величину. Это даст char диапазон от -127 до 127, что составляет всего 255 уникальных значений. (Потому что вы теряете одну полезную комбинацию битов для представления -0.)

Я не уверен, что комитет когда-либо явно обозначал это как дефект, но это произошло потому, что вы не могли полагаться на стандарт, чтобы гарантировать при передаче от unsigned char до char и обратно исходное значение сохраняется. (На практике все реализации так и поступали, потому что все они использовали дополнение до двух для целочисленных типов со знаком.)

Только недавно (C ++ 17?) Была исправлена ​​формулировка, чтобы гарантировать циклическое переключение. Это исправление, наряду со всеми другими требованиями к char, фактически требует дополнения до двух для подписанного char, не говоря об этом явно (даже если стандарт продолжает разрешать представления знак + величина для других целочисленных типов со знаком). Есть предложение потребовать, чтобы все подписанные интегральные типы использовали два дополнения, но я не помню, вошло ли это в C ++ 20.

Так что это своего рода противоположность тому, что вы ищете поскольку он дает ранее неверный чрезмерно самонадеянный код обратного исправления.

10 голосов
/ 08 августа 2020

Я не уверен, что вы сочтете это критическим изменением для правильного кода, но ...

До C ++ 11 компиляторам разрешалось, но не требовалось, исключать копии в определенных обстоятельства, даже когда конструктор копирования имеет наблюдаемые побочные эффекты. Теперь у нас есть гарантированное копирование. Поведение по существу изменилось от определенного реализацией к требуемому.

Это означает, что побочные эффекты вашего конструктора копирования могут возникать со старыми версиями, но никогда с новыми ед. Вы можете утверждать, что правильный код не должен полагаться на результаты, определяемые реализацией, но я не думаю, что это то же самое, что сказать, что такой код неверен.

7 голосов
/ 20 августа 2020

Поведение при чтении (numeri c) данных из потока и сбое чтения было изменено, начиная с c ++ 11.

Например, чтение целого числа из потока, в то время как оно не выполняется содержать целое число:

#include <iostream>
#include <sstream>

int main(int, char **) 
{
    int a = 12345;
    std::string s = "abcd";         // not an integer, so will fail
    std::stringstream ss(s);
    ss >> a;
    std::cout << "fail = " << ss.fail() << " a = " << a << std::endl;        // since c++11: a == 0, before a still 12345 
}

Так как c ++ 11 установит целое число чтения в 0 при сбое; при c ++ <11 целое число не изменилось. Тем не менее, g cc, даже при принудительном возвращении стандарта к c ++ 98 (с -std = c ++ 98) всегда показывает новое поведение, по крайней мере, с версии 4.4.7. </p>

(Imho the старое поведение было на самом деле лучше: зачем менять значение на 0, которое само по себе является действительным, когда ничего нельзя было прочитать?)

Ссылка: см. https://en.cppreference.com/w/cpp/locale/num_get/get

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...