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


104

(Я шукаю приклад чи два, щоб довести суть справи, а не список.)

Чи траплялося коли-небудь, що зміна стандарту С ++ (наприклад, з 98 на 11, 11 на 14 тощо) змінювала поведінку існуючого, чітко сформованого коду користувача з визначеною поведінкою - мовчки? тобто без попередження або помилок при компіляції з новою стандартною версією?

Примітки:

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

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

3
Я не розумію, чому це питання закрите. " Чи були коли-небудь тихі зміни поведінки в C ++ з новими стандартними версіями? ", Здається, цілком зосередженим, і суть питання, здається, не відходить від цього.
Тед Лінгмо,

На мою думку, найбільша безшумна зміна - це переосмислення auto. До C ++ 11 auto x = ...;оголосив int. Після цього він оголошує все, що ...є.
Реймонд Чен,

@RaymondChen: Ця зміна мовчить лише у тому випадку, якщо ви неявно визначали int, але явно говорили про autoзмінні were -type. Думаю, ви могли б, напевно, розрахувати з одного боку кількість людей у ​​світі, які писали б такий код, за винятком
затуманених

Правда, саме тому вони її обрали. Але це була величезна зміна в семантиці.
Реймонд Чен,

Відповіді:


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 in situ, можливо, через застарілі функції, що працюють на 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

Стандарт містить перелік основних змін у Додатку С [різниця] . Багато з цих змін можуть призвести до тихої зміни поведінки.

Приклад:

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будь-чого, що має функції-члени. Але ви не можете використовувати цю class
умову

1
@ Peter-ReinstateMonica Так, ну, ви не можете приєднати метод синтаксично в C, але це не означає, що C structнасправді POD. Як я пишу код С, більшість структур торкаються лише коду в одному файлі та функцій, що мають назву свого класу. В основному це ООП без синтаксичного цукру. Це дозволяє мені фактично контролювати, які зміни всередині 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не кажучи цього прямо (навіть якщо стандарт продовжує допускати подання знак + величина для інших підписаних цілих типів). Існує пропозиція вимагати, щоб усі підписані цілісні типи використовували доповнення two, але я не пам’ятаю, чи перетворився він на C ++ 20.

Отже, цей начебто є протилежним тому, що ви шукаєте, оскільки він дає раніше неправильний надмірно зухвалий код зворотну силу.


Частина триграфів не є відповіддю на це питання - це не тиха зміна. І, IIANM, друга частина - це зміна, визначена реалізацією, до суворо визначеної поведінки, про що я теж не те, про що я запитував.
einpoklum

10

Я не впевнений, чи вважали б ви це надзвичайною зміною виправлення коду, але ...

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

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


1
Я думав, що цю "вимогу" додали в C ++ 17, а не в C ++ 11? (Див. Тимчасову матеріалізацію .)
cdhowie

@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 доступно лише 2 перевантаження новин
Побудова виконана

Чи була така поведінка визначена як у C ++ 98, так і в C ++ 11? Або поведінка визначилася?
einpoklum

Коли cppreference.com має рацію: "якщо виникає помилка, v залишається незмінним. (До C ++ 11)" Отже, поведінка була визначена до C ++ 11 і змінена.
DanRechtsaf

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