提问人:einpoklum 提问时间:8/7/2020 最后编辑:einpoklum 更新时间:8/24/2020 访问量:6510
C++ 中是否有过新的标准版本的静默行为更改?
Have there ever been silent behavior changes in C++ with new standard versions?
问:
(我正在寻找一两个例子来证明这一点,而不是一个列表。
C++ 标准的更改(例如,从 98 到 11、11 到 14 等)是否曾经以静默方式改变了现有的、格式良好的、定义行为的用户代码的行为?即在使用较新的标准版本进行编译时没有警告或错误?
笔记:
- 我问的是标准规定的行为,而不是实现者/编译器作者的选择。
- 代码越少越好(作为这个问题的答案)。
- 我不是说具有版本检测的代码,例如.
#if __cplusplus >= 201103L
- 涉及内存模型的答案很好。
答:
在 C++ 17 中从 更改为 的返回类型。这当然会有所作为string::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。
评论
std::string
这个问题的答案显示了使用单个值初始化向量如何导致 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)。Something
v == 0
v
C++11 将默认构造每个元素。不制作任何副本。最后,向量包含 10 个对象,其值为 0 到 9(含 0 和 9)。v
评论
cv::mat
该标准在附录 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
评论
bool
u8
const char*
const char8_t*
天啊。。。cpplearner 提供的链接很可怕。
其中,C++20 不允许 C++ 结构的 C 样式结构声明。
typedef struct
{
void member_foo(); // Ill-formed since C++20
} m_struct;
如果你被教过这样的写作结构(而教“C类”的人正是教的),你就完蛋了。
评论
typedef
struct
MyClass* object = myClass_create();
struct
struct
class
class
struct
struct
structs
extern "C"
每当他们向标准库添加新方法(通常是函数)时,都会发生这种情况。
假设您有一个标准库类型:
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++ 标准不想冻结,它所能做的就是让那种默默中断的代码变得罕见或以某种方式不合理。
评论
我不确定您是否认为这是对正确代码的重大更改,但是......
在 C++11 之前,编译器被允许(但不是必需)在某些情况下省略副本,即使复制构造函数具有可观察到的副作用。现在我们有了保证的复制省略。该行为基本上从实现定义变为必需。
这意味着您的复制构造函数副作用可能在旧版本中发生,但在新版本中永远不会发生。你可能会争辩说正确的代码不应该依赖于实现定义的结果,但我认为这并不等同于说这样的代码是不正确的。
评论
三元组掉落
源文件以物理字符集进行编码,该字符集以实现定义的方式映射到标准中定义的源字符集。为了适应来自某些物理字符集的映射,这些字符集本身并不具有源字符集所需的所有标点符号,该语言定义了三元组,即三个常见字符的序列,可用于代替不太常见的标点符号。需要预处理器和编译器来处理这些。
在 C++17 中,删除了三元组。因此,某些源文件不会被较新的编译器接受,除非它们首先从物理字符集转换为一对一映射到源字符集的其他物理字符集。(在实践中,大多数编译器只是将三元组的解释作为可选。这不是一个微妙的行为更改,而是一个重大更改,它阻止了以前可接受的源文件在没有外部翻译过程的情况下被编译。
更多约束char
该标准还提到了执行字符集,该字符集由实现定义,但必须至少包含整个源字符集和少量控制代码。
C++ 标准定义为一种可能的无符号整数类型,可以有效地表示执行字符集中的每个值。在语言律师的代理下,您可以争辩说 a 必须至少为 8 位。char
char
如果您的实现使用 的无符号值 ,那么您知道它的范围可以从 0 到 255,因此适合存储每个可能的字节值。char
但是,如果您的实现使用有符号值,则它具有选项。
大多数人会使用 2 的补码,给出的最小范围是 -128 到 127。这是 256 个唯一值。char
但另一个选项是符号+幅度,其中一位保留用于指示数字是否为负数,其他七位表示幅度。这将给出 -127 到 127 的范围,即只有 255 个唯一值。(因为您丢失了一个有用的位组合来表示 -0。char
我不确定委员会是否明确地将其指定为缺陷,但这是因为你不能依靠标准来保证往返的往返会保持原始价值。(在实践中,所有实现都这样做,因为它们都使用二的补码来表示有符号整型。unsigned char
char
直到最近(C++17?)才修复了措辞以确保往返。该修复以及 的所有其他要求有效地要求了 2 的补码,而没有明确说明(即使该标准继续允许其他有符号整型的符号 + 大小表示)。有一个提议要求所有有符号的整型都使用 2 的补码,但我不记得它是否进入了 C++20。char
char
因此,这与您正在寻找的代码正好相反,因为它为以前不正确的过于冒昧的代码提供了追溯性修复。
评论
下面是一个在 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,也可以是模板声明的一部分。>>
>>
>>
评论
>>
自 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
评论
auto
auto x = ...;
int
...
auto