新标准版本的 C++ 中是否曾有过无声的行为变化?

ein*_*ica 104 c++ language-lawyer standardization

(我正在寻找一两个例子来证明这一点,而不是一个列表。)

C++ 标准的变化(例如从 98 到 11、11 到 14 等)是否曾经改变了现有的、格式良好的、定义好的用户代码的行为——默默地?即在使用较新的标准版本进行编译时没有警告或错误?

笔记:

  • 我问的是标准规定的行为,而不是实现者/编译器作者的选择。
  • 代码越少越好(作为对这个问题的回答)。
  • 我不是指具有版本检测功能的代码,例如#if __cplusplus >= 201103L.
  • 涉及内存模型的答案很好。

joh*_*ohn 112

C++ 17 中string::dataconst char*to变化的返回类型char*。这肯定会有所不同

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());
}
Run Code Online (Sandbox Code Playgroud)

有点做作,但这个合法的程序会将其输出从 C++14 更改为 C++17。

  • 无论是否人为,这都很好地展示了对格式良好的代码的更改。 (9认同)
  • 哦,我什至没有意识到 C++17 的“std::string”更改。如果有的话,我会认为 C++11 的更改可能会以某种方式导致无声的行为变化。+1。 (7认同)

cdh*_*wie 81

这个问题的答案显示了使用单个size_type值初始化向量如何导致 C++03 和 C++11 之间的行为不同。

std::vector<Something> s(10);
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

C++03 将默认构造一个Somethingv == 0然后从那个复制构造十个。最后,向量包含 10 个对象,其v值为 1 到 10,包括 1 到 10。

C++11 将默认构造每个元素。不制作任何副本。最后,向量包含 10 个对象,其v值为 0 到 9,包括 0 到 9。

  • @cdhowie 一点也不做作。我最近正在研究 UUID 课程。默认构造函数生成一个随机 UUID。我不知道这种可能性,我只是假设 C++11 的行为。 (17认同)
  • OpenCV `cv::mat` 是一个广泛使用的现实世界示例,这一点很重要。默认构造函数分配新内存,而复制构造函数为现有内存创建新视图。 (5认同)
  • 我不认为这是做作的。不同的构造函数通常在内存分配等方面表现不同。您刚刚用另一种副作用 (I/O) 替换了一种副作用。 (3认同)

cpp*_*ner 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
Run Code Online (Sandbox Code Playgroud)

  • @einpoklum 嗯,据说至少有十几个可以“改变现有代码的含义”或使它们“以不同的方式执行”。 (7认同)
  • 您如何总结这一特定变化的理由? (4认同)
  • @Nayuki 非常确定它使用“bool”版本本身并不是有意的更改,只是其他转换规则的副作用。真正的目的是阻止字符编码之间的一些混淆,实际的变化是“u8”文字过去给出“const char*”,但现在给出“const char8_t*”。 (4认同)

Yak*_*ont 25

每次他们向标准库添加新方法(通常是函数)时,都会发生这种情况。

假设您有一个标准库类型:

struct example {
  void do_stuff() const;
};
Run Code Online (Sandbox Code Playgroud)

很简单。在某些标准修订版中,添加了新方法或重载或任何内容:

struct example {
  void do_stuff() const;
  void method(); // a new method
};
Run Code Online (Sandbox Code Playgroud)

这可以默默地改变现有 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 {};
Run Code Online (Sandbox Code Playgroud)

这只是检测新的一种相对简单的方法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>{} );
}
Run Code Online (Sandbox Code Playgroud)

从类中删除方法时也会发生同样的情况。

虽然这个例子直接检测到一个方法的存在,但这种事情是间接发生的可以不那么做作。作为一个具体的例子,您可能有一个序列化引擎,它根据某事物是否可迭代,或者是否具有指向原始字节的数据和一个大小成员来决定是否可以将其序列化为容器,其中一个优先于另一个。

该标准将一个.data()方法添加到一个容器中,突然类型改变了它用于序列化的路径。

如果 C++ 标准不想冻结,那么它所能做的就是使这种默默中断的代码变得罕见或以某种方式不合理。

  • 我应该限定这个问题以排除 SFINAE,因为这不完全是我的意思……但是,是的,这是真的,所以+1。 (3认同)

Noo*_*All 15

噢男孩...链接 cpplearner提供的可怕的

其中,C++20 不允许 C++ 结构体的 C 风格结构体声明。

typedef struct
{
  void member_foo(); // Ill-formed since C++20
} m_struct;
Run Code Online (Sandbox Code Playgroud)

如果你被教导写这样的结构(而教“C with classes”的人正是这样教的),你就完蛋了

  • 教这个的人应该在黑板上写 100 遍“我不会使用 typedef 结构”。你甚至不应该用 C 语言来做这件事,恕我直言。无论如何,这种变化并不是无声的:在新标准中,[“有效的 C++ 2017 代码(在匿名非 C 结构上使用 typedef)可能格式错误”](http://eel.is/c++draft /diff#cpp17.dcl.dcl) 和 [“格式错误 - 程序存在语法错误或可诊断的语义错误。需要符合要求的 C++ 编译器才能发出诊断”](https://en.cppreference.com/w /cpp/语言/ub)。 (20认同)
  • @Peter-ReinstateMonica 好吧,我总是“typedef”我的结构,而且我肯定不会在上面浪费我的粉笔。这绝对是一个品味问题,虽然有一些非常有影响力的人(托瓦尔兹......)分享你的观点,但像我这样的其他人会指出,类型的命名约定就足够了。用“struct”关键字使代码混乱,对于大写字母(“MyClass* object = myClass_create();”)无法传达的理解几乎没有帮助。如果您希望代码中包含“struct”,我会尊重它。但我不希望它在我的身上。 (19认同)
  • 只要它们在“extern“C””块中不被禁止,我认为此更改没有任何问题。没有人应该在 C++ 中定义结构体的类型。这并不比 C++ 具有与 Java 不同的语义这一事实更大的障碍。当你学习一门新的编程语言时,你可能需要学习一些新的习惯。 (6认同)
  • 也就是说,在进行 C++ 编程时,仅对普通旧数据类型使用“struct”,对任何具有成员函数的对象使用“class”确实是一个很好的约定。但你不能在 C 中使用该约定,因为 C 中没有“类”。 (5认同)

Wax*_*rat 15

这是一个在 C++03 中打印 3 而在 C++11 中打印 0 的示例:

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'; }
Run Code Online (Sandbox Code Playgroud)

这种行为变化是由对 的特殊处理引起的>>。在 C++11 之前,>>始终是右移运算符。使用 C++11,>>也可以成为模板声明的一部分。


Adr*_*thy 11

三合字母下降

源文件以物理字符集编码,该字符集以实现定义的方式映射到标准中定义的源字符集。为了适应某些物理字符集的映射,这些物理字符集本身并不具有源字符集所需的所有标点符号,该语言定义了三合字母——三个常见字符的序列,可以用来代替一个不太常见的标点符号。预处理器和编译器需要处理这些。

在 C++17 中,删除了三合字母。因此,某些源文件不会被较新的编译器接受,除非它们首先从物理字符集转换为其他一些物理字符集,该物理字符集一对一地映射到源字符集。(实际上,大多数编译器只是将三合字母的解释设为可选。)这不是一个微妙的行为变化,而是一个破坏性的变化,它阻止了以前可接受的源文件在没有外部翻译过程的情况下被编译。

更多限制 char

该标准还提到了执行字符集,它是实现定义的,但必须至少包含整个源字符集加上少量的控制代码。

C++ 标准定义char为可能无符号整数类型,可以有效地表示执行字符集中的每个值。根据语言律师的陈述,您可以争辩说 achar必须至少为 8 位。

如果您的实现对 使用无符号值char,那么您知道它的范围可以从 0 到 255,因此适合存储每个可能的字节值。

但是,如果您的实现使用有符号值,则它具有选项。

大多数会使用二进制补码,char最小范围为 -128 到 127。这是 256 个唯一值。

但另一种选择是符号+幅度,其中一位保留用于指示数字是否为负,其他七位指示幅度。这将给出char-127 到 127 的范围,即只有 255 个唯一值。(因为你失去了一个有用的位组合来表示 -0。)

我不知道该委员会曾明确指定这是一个缺陷,但它是因为你不能依赖标准从保证往返unsigned charchar和背部会保留原始值。(实际上,所有实现都这样做,因为它们都使用二进制补码来表示有符号整数类型。)

直到最近(C++17?)才修复了措辞以确保往返。该修复以及关于 的所有其他要求char,有效地强制要求对有符号的补码进行补码,char而无需明确说明(即使标准继续允许其他有符号整数类型的符号+大小表示)。有一个提议要求所有有符号整数类型都使用二进制补码,但我不记得它是否已进入 C++20。

所以这个与你正在寻找的有点相反,因为它为以前不正确的 过度自以为是的代码提供了追溯修复。


Adr*_*thy 10

我不确定您是否认为这是对正确代码的重大更改,但是......

在 C++11 之前,编译器被允许但不是必需的,在某些情况下省略副本,即使复制构造函数具有可观察到的副作用。现在我们保证了复制省略。行为本质上是从实现定义到必需的。

这意味着您的复制构造函数副作用可能在旧版本中发生,但在新版本中永远不会发生。您可能会争辩说正确的代码不应该依赖于实现定义的结果,但我认为这与说此类代码不正确完全相同。


Dan*_*saf 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 
}
Run Code Online (Sandbox Code Playgroud)

由于 c++ 11 会在失败时将读取的整数设置为 0;在 c++ < 11 时,整数没有改变。也就是说,即使强制标准回到 c++98(使用 -std=c++98 ),gcc 也总是显示至少自 4.4.7 版以来的新行为。

(恕我直言,旧行为实际上更好:为什么在无法读取任何内容时将值更改为 0,这本身就是有效的?)

参考:见https://en.cppreference.com/w/cpp/locale/num_get/get