C++ 中是否有过新的标准版本的静默行为更改?

Have there ever been silent behavior changes in C++ with new standard versions?

提问人:einpoklum 提问时间:8/7/2020 最后编辑:einpoklum 更新时间:8/24/2020 访问量:6510

问:

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

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

笔记:

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

评论

0赞 Samuel Liew 8/9/2020
评论不用于扩展讨论;此对话已移至 Chat
0赞 Raymond Chen 8/21/2020
在我看来,最大的无声的突破性变化是重新定义了 .在 C++11 之前,声明了一个 .之后,它声明任何内容。autoauto x = ...;int...
0赞 einpoklum 8/21/2020
@RaymondChen:仅当您隐式定义 int 但明确说出 were 类型变量时,此更改才是无声的。我想你可以用一只手来数一数世界上有多少人会写这种代码,除了混淆的 C 代码竞赛......auto
0赞 Raymond Chen 8/21/2020
没错,这就是他们选择它的原因。但这在语义上发生了巨大的变化。

答:

113赞 john 8/7/2020 #1

在 C++ 17 中从 更改为 的返回类型。这当然会有所作为string::dataconst char*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());
}

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

评论

7赞 einpoklum 8/7/2020
哦,我什至没有意识到 C++17 的变化。如果有的话,我会认为 C++11 的更改可能会以某种方式导致无声的行为变化。+1.std::string
9赞 David C. Rankin 8/7/2020
不管是不是人为的,这很好地展示了对格式良好的代码的更改。
0赞 Peter - Reinstate Monica 8/9/2020
顺便说一句,当您就地更改 std::string 的内容时,更改是基于有趣但合法的用例,可能是通过在 char * 上运行的遗留函数。现在这是完全合法的:就像向量一样,可以保证有一个底层的、连续的数组,你可以操纵它(你总是可以通过返回的引用;现在它变得更加自然和明确)。可能的用例是可编辑的、固定长度的数据集(例如某种消息),如果基于 std:: 容器,则保留 STL 的服务,如生命周期管理、可复制性等。
82赞 cdhowie 8/7/2020 #2

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

std::vector<Something> s(10);

C++03 默认构造元素类型的临时对象,并从该临时对象复制构造向量中的每个元素。Something

C++11 默认构造向量中的每个元素。

在许多(大多数?)情况下,这些会导致等效的最终状态,但没有理由必须这样做。这取决于 的 default/copy 构造函数的实现。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 将默认构造一个,然后从该构造中复制构造另外十个。最后,向量包含 10 个对象,其值为 1 到 10(含 10)。Somethingv == 0v

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

评论

0赞 cdhowie 8/7/2020
不过,@einpoklum我添加了一个人为的例子。:)
3赞 einpoklum 8/7/2020
我不认为这是人为的。不同的构造函数通常以不同的方式执行内存分配等操作。您只是用另一种副作用 (I/O) 替换了一种副作用。
17赞 john 8/7/2020
@cdhowie 一点也不做作。我最近在做一个UUID课程。默认构造函数生成一个随机 UUID。我不知道这种可能性,我只是假设 C++11 的行为。
5赞 jpa 8/8/2020
一个广泛使用的现实世界中很重要的类示例是 OpenCV 。默认构造函数分配新内存,而复制构造函数为现有内存创建新视图。cv::mat
0赞 David Waterworth 8/9/2020
我不会称其为人为的例子,它清楚地表明了行为的差异。
52赞 cpplearner 8/7/2020 #3

该标准在附录 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赞 cpplearner 8/7/2020
@einpoklum 好吧,据说其中至少有十几个可以“改变现有代码的含义”或使它们“以不同的方式执行”。
4赞 Nayuki 8/8/2020
您如何总结这一特定变化的基本原理?
4赞 leftaroundabout 8/8/2020
@Nayuki很确定它使用该版本本身并不是预期的更改,只是其他转换规则的副作用。真正的目的是阻止字符编码之间的一些混淆,实际的变化是字面上曾经给出但现在给出。boolu8const char*const char8_t*
15赞 Noone AtAll 8/7/2020 #4

天啊。。。cpplearner 提供的链接可怕

其中,C++20 不允许 C++ 结构的 C 样式结构声明。

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

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

评论

20赞 Peter - Reinstate Monica 8/7/2020
无论谁教过它,都应该在黑板上写 100 次“我不会打字定义结构”。恕我直言,你甚至不应该用 C 语言来做。无论如何,这种变化并不是无声的:在新标准中,“有效的 C++ 2017 代码(在匿名、非 C 结构上使用 typedef)可能格式错误”和“格式错误 - 程序有语法错误或可诊断的语义错误。需要符合 C++ 的编译器才能发出诊断”。
19赞 cmaster - reinstate monica 8/7/2020
@Peter-ReinstateMonica 好吧,我总是我的结构,我肯定不会在上面浪费我的粉笔。这绝对是一个品味问题,虽然有一些非常有影响力的人(Torvalds...)分享你的观点,但像我这样的其他人会指出,只需要一个类型的命名约定。用关键字弄乱代码对理解大写字母 () 无法传达的理解几乎没有增加。如果你想在你的代码中使用它,我尊重它。但我不想把它放在我的身上。typedefstructMyClass* object = myClass_create();struct
5赞 cmaster - reinstate monica 8/7/2020
也就是说,在对 C++ 进行编程时,仅用于纯旧数据类型以及任何具有成员函数的类型确实是一个很好的约定。但是你不能在 C 中使用这个约定,因为 C 中没有。structclassclass
1赞 cmaster - reinstate monica 8/7/2020
@Peter-ReinstateMonica 是的,你不能在 C 语法上附加方法,但这并不意味着 C 实际上是 POD。我编写 C 代码的方式是,大多数结构仅由单个文件中的代码和带有其类名称的函数所触及。它基本上是没有句法糖的 OOP。这允许我实际控制 内部的变化,以及其成员之间保证哪些不变量。因此,我倾向于使用成员函数、私有实现、不变量以及从其数据成员中抽象出来。听起来不像 POD,是吗?structstructstructs
6赞 Cody Gray - on strike 8/8/2020
只要它们在块中不被禁止,我看不出此更改有任何问题。没有人应该在 C++ 中对结构进行类型定义。这并不比C++具有与Java不同的语义这一事实更大的障碍。当你学习一门新的编程语言时,你可能需要学习一些新的习惯。extern "C"
25赞 Yakk - Adam Nevraumont 8/8/2020 #5

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

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

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赞 einpoklum 8/8/2020
我应该限定这个问题以排除 SFINAE,因为这不是我的意思......但是,是的,这是真的,所以+1。
0赞 Ian Ringrose 8/8/2020
“这种事情间接发生”导致了赞成票而不是反对票,因为这是一个真正的陷阱。
1赞 cdhowie 8/10/2020
这是一个很好的例子。尽管 OP 打算排除它,但这可能是最有可能导致对现有代码进行静默行为更改的事情之一。+1
1赞 Yakk - Adam Nevraumont 8/14/2020
@TedLyngmo 如果无法修复检测器,请更换检测到的检测器。德克萨斯州神枪手!
10赞 Adrian McCarthy 8/9/2020 #6

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

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

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

评论

1赞 cdhowie 8/10/2020
我以为这个“要求”是在 C++17 中添加的,而不是 C++11?(请参阅临时物化
0赞 Adrian McCarthy 8/11/2020
@cdhowie:我认为你是对的。当我写这篇文章时,我手头没有标准,我可能过于信任我的一些搜索结果。
0赞 einpoklum 8/18/2020
对实现定义的行为的更改不算作此问题的答案。
11赞 Adrian McCarthy 8/9/2020 #7

三元组掉落

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

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

更多约束char

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

C++ 标准定义为一种可能的无符号整数类型,可以有效地表示执行字符集中的每个值。在语言律师的代理下,您可以争辩说 a 必须至少为 8 位。charchar

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

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

大多数人会使用 2 的补码,给出的最小范围是 -128 到 127。这是 256 个唯一值。char

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

我不确定委员会是否明确地将其指定为缺陷,但这是因为你不能依靠标准来保证往返的往返会保持原始价值。(在实践中,所有实现都这样做,因为它们都使用二的补码来表示有符号整型。unsigned charchar

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

因此,这与您正在寻找的代码正好相反,因为它为以前不正确的过于冒昧的代码提供了追溯性修复。

评论

0赞 einpoklum 8/18/2020
三元组部分不是这个问题的答案——这不是一个无声的改变。而且,IIANM,第二部分是将实现定义的行为更改为严格强制的行为,这也不是我所问的。
15赞 Waxrat 8/12/2020 #8

下面是一个在 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'; }

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

评论

0赞 einpoklum 8/18/2020
好吧,从技术上讲这是真的,但由于使用了这种方式,这段代码一开始就“非正式地模棱两可”。>>
7赞 DanRechtsaf 8/20/2020 #9

自 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 (with -std=c++98 ),至少从版本 4.4.7 开始总是显示新行为。

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

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

评论

0赞 Build Succeeded 8/20/2020
但是没有提到关于 returnType 的变化。自 C++11 以来只有 2 个新闻重载可用
0赞 einpoklum 8/20/2020
这种行为在 C++98 和 C++11 中都定义了吗?还是行为被定义了?
0赞 DanRechtsaf 8/22/2020
当 cppreference.com 正确时:“如果发生错误,v 保持不变。(直到 C++11)“ 所以行为是在 C++11 之前定义的,并且发生了变化。
0赞 Rasmus Damgaard Nielsen 8/23/2020
根据我的理解,ss > a 的行为确实被定义了,但对于您正在读取未初始化变量的非常常见情况,c++ 11 行为将使用未初始化的变量,即未定义的行为。因此,失败的默认构造可以防止非常常见的未定义行为。