103

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

C++ 标准的变化(例如,从 98 到 11、11 到 14 等)是否曾经发生过这样的情况:无声地改变了现有的、格式良好的、定义行为的用户代码的行为?即使用较新的标准版本编译时没有警告或错误?

笔记:

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

9 回答 9

112

C++ 17 中从tostring::data更改的返回类型。这肯定会有所作为const 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。

于 2020-08-06T20:36:58.653 回答
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 将默认构造一个Somethingv == 0然后从那个复制构造十个。最后,该向量包含 10 个对象,其v值为 1 到 10(包括 1 到 10)。

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

于 2020-08-06T21:24:38.387 回答
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
于 2020-08-07T10:14:51.177 回答
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>{} );
}

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

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

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

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

于 2020-08-07T20:31:38.633 回答
15

哦,男孩... cpplearner提供的链接吓人

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

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

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

于 2020-08-07T13:29:36.193 回答
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'; }

这种行为变化是由对>>. 在 C++11 之前,>>始终是右移运算符。对于 C++11,>>也可以是模板声明的一部分。

于 2020-08-11T17:27:58.963 回答
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。

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

于 2020-08-08T19:01:35.537 回答
10

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

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

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

于 2020-08-08T18:11:38.280 回答
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

于 2020-08-20T13:34:56.960 回答