Были ли когда-нибудь изменения в тихом поведении 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

Другие вопросы по тегам