Были ли когда-нибудь изменения в тихом поведении C++ с новыми стандартными версиями?
(Я ищу пару примеров, чтобы доказать свою точку зрения, а не список.)
Было ли когда-нибудь изменение стандарта C++ (например, с 98 на 11, с 11 на 14 и т. Д.) Изменяло поведение существующего, правильно сформированного пользовательского кода с определенным поведением - незаметно? т.е. без предупреждения или ошибок при компиляции с более новой стандартной версией?
Примечания:
- Я спрашиваю о поведении в соответствии со стандартами, а не о выборе автора / разработчика компилятора.
- Чем менее надуманный код, тем лучше (как ответ на этот вопрос).
- Я не имею в виду код с определением версии, такой как
#if __cplusplus >= 201103L
. - Ответы, связанные с моделью памяти, прекрасны.
9 ответов
Тип возврата 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.
Ответ на этот вопрос показывает, как инициализировать вектор с помощью одногоsize_type
value может привести к различному поведению между 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 включительно.
В стандарте есть список критических изменений в Приложении 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
Это происходит каждый раз, когда они добавляют новые методы (а часто и функции) в стандартную библиотеку.
Предположим, у вас есть библиотека стандартного типа:
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++ может сделать, если он не хочет "зависать", - это сделать код, который молча ломается, редким или каким-то образом необоснованным.
Вот пример, который печатает 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,>>
также может быть частью объявления шаблона.
О мальчик... Ссылка cpplearner при условии, это страшно.
Среди прочего, C++20 запретил объявление структур в стиле C для структур C++.
typedef struct
{
void member_foo(); // Ill-formed since C++20
} m_struct;
Если вас научили писать такие структуры (а люди, которые преподают "C с классами", учат именно этому), вы облажались.
Триграфы упали
Исходные файлы кодируются в физическом наборе символов, который в зависимости от реализации сопоставляется с исходным набором символов, который определен в стандарте. Для соответствия сопоставлениям из некоторых физических наборов символов, которые изначально не имели всех знаков препинания, необходимых для исходного набора символов, язык определил триграфы - последовательности из трех общих символов, которые можно было использовать вместо менее распространенных знаков пунктуации. Для их обработки требовались препроцессор и компилятор.
В C++17 триграфы были удалены. Таким образом, некоторые исходные файлы не будут приняты более новыми компиляторами, если они не будут сначала переведены из физического набора символов в какой-либо другой физический набор символов, который однозначно отображает исходный набор символов. (На практике большинство компиляторов просто сделали интерпретацию триграфов необязательной.) Это не тонкое изменение поведения, а критическое изменение, предотвращающее компиляцию ранее приемлемых исходных файлов без внешнего процесса перевода.
Больше ограничений на char
Стандарт также относится к набору символов выполнения, который определяется реализацией, но должен содержать по крайней мере весь исходный набор символов плюс небольшое количество управляющих кодов.
Стандарт C++ определен char
как возможно беззнаковый целочисленный тип, который может эффективно представлять каждое значение в наборе символов выполнения. С представлением языкового юриста вы можете утверждать, чтоchar
должно быть не менее 8 бит.
Если ваша реализация использует беззнаковое значение для char
, то вы знаете, что он может находиться в диапазоне от 0 до 255 и, следовательно, подходит для хранения всех возможных байтовых значений.
Но если ваша реализация использует значение со знаком, у нее есть варианты.
Большинство используют дополнение до двух, давая char
минимальный диапазон от -128 до 127. Это 256 уникальных значений.
Но другой вариант - знак + величина, где один бит зарезервирован, чтобы указать, является ли число отрицательным, а остальные семь битов указывают величину. Это дастchar
диапазон от -127 до 127, что составляет всего 255 уникальных значений. (Потому что вы теряете одну полезную комбинацию битов для представления -0.)
Я не уверен, что комитет когда-либо явным образом обозначил это как дефект, но это произошло потому, что вы не могли полагаться на стандарт, чтобы гарантировать обратный путь от unsigned char
к char
и обратно сохранит исходное значение. (На практике все реализации использовали, потому что все они использовали дополнение до двух для целочисленных типов со знаком.)
Только недавно (C++17?) Была исправлена формулировка, обеспечивающая циклическое переключение. Это исправление вместе со всеми другими требованиями кchar
, фактически требует дополнения до двух для подписанных char
не говоря об этом явно (даже если стандарт продолжает разрешать представления знак + величина для других целочисленных типов со знаком). Есть предложение потребовать, чтобы все подписанные интегральные типы использовали два дополнения, но я не помню, вошло ли это в C++20.
Так что это своего рода противоположность тому, что вы ищете, потому что оно дает ранее некорректный и чрезмерно самонадеянный код обратного исправления.
Я не уверен, что вы считаете это критическим изменением правильного кода, но...
До C++11 компиляторам разрешалось, но не требовалось, исключать копии при определенных обстоятельствах, даже когда конструктор копирования имел наблюдаемые побочные эффекты. Теперь у нас есть гарантированное копирование. По сути, поведение изменилось от определенного реализацией к требуемому.
Это означает, что побочные эффекты конструктора копирования могли возникнуть в старых версиях, но никогда не появятся в новых. Вы можете утверждать, что правильный код не должен полагаться на результаты, определяемые реализацией, но я не думаю, что это то же самое, что сказать, что такой код неверен.
Поведение при чтении (числовых) данных из потока и сбое чтения было изменено, начиная с 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 целое число не изменилось. Тем не менее, gcc, даже при принудительном возврате стандарта к C++98 (с -std= C++98), всегда показывает новое поведение, по крайней мере, с версии 4.4.7.
(Имхо, старое поведение было на самом деле лучше: зачем менять значение на 0, которое само по себе действительно, когда ничего нельзя было прочитать?)
Ссылка: см. https://en.cppreference.com/w/cpp/locale/num_get/get