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


104

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

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

Примечания:

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

Комментарии не предназначены для расширенного обсуждения; этот разговор был перемещен в чат .
Сэмюэл Лью

3
Я не понимаю, почему этот вопрос закрыт. « Были ли когда-либо изменения в поведении C ++ без звука с новыми стандартными версиями? » Кажется совершенно сфокусированным, и основная часть вопроса не отклоняется от этого.
Тед Люнгмо,

На мой взгляд, самое большое бесшумное изменение - это переопределение auto. До C ++ 11 auto x = ...;объявленный int. После он объявляет все, что ...есть.
Раймонд Чен,

@RaymondChen: это изменение молчит, только если вы неявно определяли int, но явно autoуказали переменные типа-типа. Думаю, вы могли бы сосчитать по пальцам одну руку, количество людей в мире, которые напишут такой код, за исключением конкурсов запутанного кода C ...
einpoklum

Правда, поэтому и выбрали. Но это было огромное изменение в семантике.
Раймонд Чен,

Ответы:


113

Тип возвращаемого значения 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.


7
О, я даже не осознавал, что это были std::stringизменения для C ++ 17. Во всяком случае, я бы подумал, что изменения C ++ 11 могли каким-то образом вызвать изменение тихого поведения. +1.
einpoklum

9
Надуманный или нет, это довольно хорошо демонстрирует изменение правильного кода.
Дэвид С. Ранкин,

Кроме того, это изменение основано на забавных, но законных случаях использования, когда вы изменяете содержимое std :: string на месте, возможно, с помощью устаревших функций, работающих с char *. Теперь это совершенно законно: как и в случае с вектором, есть гарантия, что существует лежащий в основе непрерывный массив, которым вы можете манипулировать (вы всегда могли использовать возвращаемые ссылки; теперь он стал более естественным и явным). Возможные варианты использования - редактируемые наборы данных фиксированной длины (например, сообщения какого-либо вида), которые, если они основаны на std :: container, сохраняют такие услуги STL, как управление временем жизни, возможность копирования и т. Д.
Питер - Восстановите Монику

81

Ответ на этот вопрос показывает, как инициализация вектора с использованием одного 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 включительно.


@einpoklum Однако я добавил надуманный пример. :)
cdhowie

3
Не думаю, что это надумано. Разные конструкторы часто действуют по-разному, например, в отношении выделения памяти. Вы просто заменили один побочный эффект другим (ввод-вывод).
einpoklum

17
@cdhowie Совсем не надуманный. Недавно я работал над классом UUID. Конструктор по умолчанию генерирует случайный UUID. Я понятия не имел об этой возможности, я просто предполагал поведение C ++ 11.
Иоанн

5
Одним из широко используемых примеров реального мира, где это имеет значение, является OpenCV cv::mat. Конструктор по умолчанию выделяет новую память, а конструктор копирования создает новое представление для существующей памяти.
jpa

Я бы не назвал это надуманным примером, он наглядно демонстрирует разницу в поведении.
Дэвид Уотеруорт,

51

В стандарте есть список критических изменений в Приложении 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

7
@einpoklum Что ж, по крайней мере, дюжина из них, как говорят, «меняют смысл» существующего кода или заставляют их «выполнять по-другому».
cpplearner

4
Как бы вы суммировали обоснование этого конкретного изменения?
Наюки

4
@Nayuki уверен, что использование этой boolверсии не было намеренным изменением само по себе, а просто побочным эффектом других правил преобразования. Настоящее намерение заключалось в том, чтобы устранить некоторую путаницу между кодировками символов, фактическое изменение заключается в том, что u8литералы давали, const char*а теперь дают const char8_t*.
налево примерно

25

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

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

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
Я должен был квалифицировать вопрос, чтобы исключить SFINAE, потому что это не совсем то, что я имел в виду ... но да, это правда, так что +1.
einpoklum

«такого рода вещи происходят косвенно» привели к голосованию «за», а не «против», поскольку это настоящая ловушка.
Ян Рингроуз,

1
Это действительно хороший пример. Несмотря на то, что OP намеревался исключить его, это, вероятно, одна из наиболее вероятных причин, вызывающих изменения в тихом поведении существующего кода. +1
cdhowie

1
@TedLyngmo Если вы не можете починить детектор, замените обнаруженную вещь. Техасская меткая стрельба!
Якк - Адам Неврамонт,

15

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

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

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

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



19
@ Peter-ReinstateMonica Ну, у меня всегда есть typedefсвои структуры, и я определенно не собираюсь тратить на это свой мел. Это определенно вопрос вкуса, и хотя есть очень влиятельные люди (Торвальдс ...), которые разделяют вашу точку зрения, другие люди, такие как я, укажут, что все, что нужно, - это соглашение об именах для типов. Загромождение кода structключевыми словами мало что добавляет к пониманию того, что заглавная буква ( MyClass* object = myClass_create();) не передает. Я уважаю это, если вы хотите, чтобы structв вашем коде. Но я не хочу этого в своем.
cmaster - восстановить Монику

5
Тем не менее, при программировании на C ++ это действительно хорошее соглашение, которое следует использовать structтолько для простых старых типов данных и classвсего, что имеет функции-члены. Но вы не можете использовать это соглашение в C, как и classв C.
cmaster - восстановить монику

1
@ Peter-ReinstateMonica Да, вы не можете присоединить метод синтаксически в C, но это не значит, что C structна самом деле является POD. Как я пишу код C, большинство структур затрагиваются только кодом в одном файле и функциями, которые несут имя своего класса. По сути, это ООП без синтаксического сахара. Это позволяет мне фактически контролировать, какие изменения внутри a structи какие инварианты гарантируются между его членами. Итак, у меня, structsкак правило, есть функции-члены, частная реализация, инварианты и абстракция от их элементов данных. Не похоже на POD?
cmaster - восстановить Монику

5
Пока они не запрещены в extern "C"блоках, я не вижу никаких проблем с этим изменением. Никто не должен заниматься типизацией структур в C ++. Это не более серьезное препятствие, чем тот факт, что C ++ имеет другую семантику, чем Java. Когда вы изучаете новый язык программирования, вам может потребоваться освоить новые привычки.
Коди Грей

15

Вот пример, который печатает 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 >>тоже может быть частью объявления шаблона.


Что ж, технически это правда, но этот код изначально был «неформально неоднозначным» из-за использования >>этого способа.
einpoklum

11

Триграфы упали

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

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

Больше ограничений на char

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

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

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

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

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

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

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

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

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


Часть триграфов не является ответом на этот вопрос - это не тихое изменение. И, IIANM, вторая часть - это изменение поведения, определенного реализацией, на строго предписанное, что тоже не то, о чем я спрашивал.
einpoklum

10

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

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

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


1
Я думал, это «требование» было добавлено в C ++ 17, а не в C ++ 11? (См .
Временную

@cdhowie: Думаю, ты прав. Когда я писал это, у меня не было под рукой стандартов, и я, вероятно, слишком доверяю некоторым своим результатам поиска.
Адриан Маккарти,

Изменение поведения, определяемого реализацией, не считается ответом на этот вопрос.
einpoklum

7

Поведение при чтении (числовых) данных из потока и сбое чтения было изменено, начиная с 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


Но о returnType не упоминается никаких изменений. С C ++ 11 доступны только две перегрузки новостей
сборка завершена

Было ли такое поведение определено как в C ++ 98, так и в C ++ 11? Или поведение стало определенным?
einpoklum

Когда cppreference.com прав: «при возникновении ошибки v остается без изменений (до C ++ 11)» Таким образом, поведение было определено до C ++ 11 и изменилось.
DanRechtsaf

Насколько я понимаю, поведение для ss> a действительно было определено, но для очень распространенного случая, когда вы читаете неинициализированную переменную, поведение С ++ 11 будет использовать неинициализированную переменную, что является неопределенным поведением. Таким образом, конструкция по умолчанию при сбое защищает от очень распространенного неопределенного поведения.
Расмус Дамгаард Нильсен,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.