(我正在寻找一个或两个例子来证明这一点,而不是清单。)
是否曾经发生过C ++标准的更改(例如从98变为11、11到14等)以无声的方式改变了现有的,格式正确的,定义了行为的用户代码的行为?即在使用较新的标准版本进行编译时没有警告或错误?
笔记:
- 我问的是标准规定的行为,而不是实现者/编译器作者的选择。
- 编写的代码越少越好(作为对此问题的答案)。
- 我并不是说带有版本检测功能的代码,例如
#if __cplusplus >= 201103L
。 - 涉及内存模型的答案很好。
(我正在寻找一个或两个例子来证明这一点,而不是清单。)
是否曾经发生过C ++标准的更改(例如从98变为11、11到14等)以无声的方式改变了现有的,格式正确的,定义了行为的用户代码的行为?即在使用较新的标准版本进行编译时没有警告或错误?
笔记:
#if __cplusplus >= 201103L
。auto
。在C ++ 11之前,auto x = ...;
声明一个int
。之后,它声明什么...
。
auto
-type变量,此更改才是无声的。我认为您也许可以一方面指望世界上会编写这种代码的人数,除了令人困惑的C代码竞赛...
Answers:
在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 ++ 17的变化。如果有的话,我会认为C ++ 11的更改可能会导致静默行为更改。+1。
这个问题的答案显示了使用单个size_type
值初始化向量如何导致C ++ 03和C ++ 11之间的不同行为。
std::vector<Something> s(10);
C ++ 03默认构造一个元素类型的临时对象,Something
并从该临时对象复制构造向量中的每个元素。
C ++ 11默认构造向量中的每个元素。
在许多(大多数?)情况下,这些结果会导致最终状态相等,但没有必要这样做。它取决于Something
的default / copy构造函数的实现。
看到这个人为的例子:
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(含1和10)。
C ++ 11将默认构造每个元素。没有副本。最后,向量包含10个对象,其v
值从0到9(含0和9)。
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*
。
每当他们向标准库添加新方法(通常是函数)时,都会发生这种情况。
假设您有一个标准的库类型:
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 ++标准可以做的所有事情就是使那种默默中断的代码变得稀少或不合理。
哦,男孩... cpplearner提供的链接 很可怕。
除其他外,C ++ 20不允许C ++结构的C样式结构声明。
typedef struct
{
void member_foo(); // Ill-formed since C++20
} m_struct;
如果您被教导要编写这样的结构(而教“用类学习C”的人正是这样教的),那您真是无所适从。
typedef
我的结构,而且我当然不会浪费我的粉笔。这绝对是一个品味问题,尽管有极有影响力的人(Torvalds ...)分享您的观点,但像我这样的其他人也会指出,只需要类型的命名约定即可。用struct
关键字使代码混乱不堪,这对大写字母(MyClass* object = myClass_create();
)无法传达的理解几乎没有帮助。如果您要struct
在代码中使用,我会尊重您的。但是我不想要它。
struct
仅用于纯旧数据类型以及class
任何具有成员函数的类型。但是你不能用用C该公约,因为没有class
在C.
struct
实际上是POD。在我编写C代码的方式中,大多数结构只能通过单个文件中的代码以及带有其类名的函数来实现。它基本上是没有语法糖的OOP。这使我可以实际控制a内部的哪些变化struct
以及在其成员之间保证哪些不变性。因此,我structs
倾向于具有成员函数,私有实现,不变式和来自其数据成员的抽象。听起来不像POD,不是吗?
extern "C"
,我就不会发现此更改有任何问题。没有人应该在C ++中对类型进行定义。这比C ++的语义不同于Java的事实更大。当您学习一种新的编程语言时,您可能需要学习一些新习惯。
这是一个示例,在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 ++ 17中,删除了三字组合。因此,某些源文件将不被较新的编译器接受,除非它们首先从物理字符集转换为将一对一映射到源字符集的其他物理字符集。(在实践中,大多数编译器只是使对trigraph的解释成为可选的。)这不是微妙的行为更改,但是重大更改阻止了以前可以接受的源文件在没有外部翻译过程的情况下进行编译。
char
该标准还引用了执行字符集,它是由实现定义的,但必须至少包含整个源字符集以及少量控制代码。
C ++标准定义char
为可能无符号的整数类型,可以有效地表示执行字符集中的每个值。使用语言律师的代理,您可以辩称achar
必须至少为8位。
如果您的实现使用的无符号值char
,那么您知道它的范围可以从0到255,因此适合存储每个可能的字节值。
但是,如果您的实现使用带符号的值,则它具有选项。
大多数会使用二进制补码,char
最小范围为-128到127。即256个唯一值。
但是另一种选择是符号+幅度,其中保留一位以指示数字是否为负,其余七个位指示幅度。那将给出char
-127到127的范围,这只有255个唯一值。(因为您丢失了一个有用的位组合来表示-0。)
我不知道该委员会曾明确指定这是一个缺陷,但它是因为你不能依赖标准从保证往返unsigned char
于char
和背部会保留原始值。(实际上,所有实现都是这样做的,因为它们都对符号整数类型使用了二进制补码。)
仅在最近(C ++ 17?)才修订该措词以确保往返。该修补程序以及上的所有其他要求char
,有效地要求对带符号的二进制补码,char
而不必明确说明(即使标准继续允许对其他带符号整数类型使用正负号表示)。有人提出要求所有带符号的整数类型都使用二进制补码,但是我不记得它是否已纳入C ++ 20。
因此,这与您要查找的内容有点相反,因为它为以前不正确的 过分冒犯的代码提供了追溯修复。
从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时,整数未更改。也就是说,即使在将标准强制回至c ++ 98(带有-std = c ++ 98)的情况下,gcc至少从4.4.7版本开始总是显示出新的行为。
(恕我直言,以前的行为实际上更好:为什么什么都看不到,为什么将值更改为0本身就是有效的?)