使用新的标准版本的C ++中是否有过无声的行为更改?


104

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

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

笔记:

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

评论不作进一步讨论;此对话已转移至聊天
塞缪尔·刘

3
我不明白为什么这个问题已经解决。“在C ++中使用新的标准版本进行过无声的行为更改吗? ”似乎完全专注于此,问题的主体似乎并没有因此而偏离。
Ted Lyngmo

在我看来,最大的无声突破性变化是的重新定义auto。在C ++ 11之前,auto x = ...;声明一个int。之后,它声明什么...
雷蒙·陈

@RaymondChen:仅当您隐式定义int时,而显式说出are auto-type变量,此更改才是无声的。我认为您也许可以一方面指望世界上会编写这种代码的人数,除了令人困惑的C代码竞赛...
einpoklum

没错,这就是他们选择它的原因。但这是语义上的巨大变化。
雷蒙·陈

Answers:


113

在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
哦,我什至都没有意识到std::stringC ++ 17的变化。如果有的话,我会认为C ++ 11的更改可能会导致静默行为更改。+1。
einpoklum

9
不论是否设计,这都很好地说明了对格式正确的代码的更改。
David C. Rankin

顺便说一句,当您原位更改std :: string的内容时,更改可能基于有趣但合理的用例也许是通过对char *进行操作的旧函数实现的。现在这完全是合法的:与向量一样,可以保证存在一个可以操作的底层连续数组(您始终可以通过返回的引用进行操作;现在,它变得更加自然和明确)。可能的使用情况是可编辑的,固定长度的数据集(例如某种消息),如果基于一个std ::容器,保留了STL的服务,如生活时间的管理,复制能力等
彼得-恢复莫妮卡

81

这个问题的答案显示了使用单个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将默认构造一个Somethingv == 0然后再从该构造中再拷贝十个。最后,向量包含十个对象,其v值是1到10(含1和10)。

C ++ 11将默认构造每个元素。没有副本。最后,向量包含10个对象,其v值从0到9(含0和9)。


@einpoklum我添加了一个人为的示例。:)
cdhowie

3
我认为这不是故意的。不同的构造函数通常在诸如内存分配之类的事情上采取不同的行动。您只是将一种副作用替换为另一种(I / O)。
einpoklum

17
@cdhowie根本不做。我最近正在研究UUID类。默认构造函数生成一个随机UUID。我不知道这种可能性,我只是假设C ++ 11的行为。
约翰,

5
OpenCV是其中一个很重要的现实世界中广泛使用的示例cv::mat。默认构造函数分配新的内存,而副本构造函数为现有内存创建一个新视图。
jpa

我不会说这个人为的例子,它清楚地说明了行为上的差异。
大卫·沃特沃思

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

7
@einpoklum好吧,据说至少有十二个“改变了”现有代码的含义或使它们“以不同的方式执行”。
cpplearner

4
您如何总结此特定更改的理由?
Nayuki '20

4
@Nayuki非常确定使用该bool版本本身不是预期的更改,而只是其他转换规则的副作用。真正的目的是阻止字符编码之间的某些混淆,实际的更改是原义的u8文字,const char*但现在给出const char8_t*
左右左转

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 ++标准可以做的所有事情就是使那种默默中断的代码变得稀少或不合理。


3
我应该已经排除了SFINAE的问题,因为这不是我的意思……但是,是的,所以+1。
einpoklum

“这种间接发生的事情”导致了赞成而不是反对,因为这是一个真正的陷阱。
伊恩·林格罗斯

1
这是一个很好的例子。即使OP打算排除它,这也可能是导致对现有代码进行静默行为更改的有可能的事情之一。+1
cdhowie

1
@TedLyngmo如果无法修复检测器,请更换检测到的东西。德州神枪手!
Yakk-Adam Nevraumont

15

哦,男孩... cpplearner提供的链接可怕

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

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

如果您被教导要编写这样的结构(而教“用类学习C”的人正是这样教的),那您真是无所适从


20
那些教过的人应该在黑板上写下100次“我不会打字定义结构”。恕我直言,您甚至不应该在C语言中执行此操作。无论如何,这种变化并不是无言的:在新标准中,“有效的C ++ 2017代码(在匿名,非C结构上使用typedef可能会格式错误)“格式错误-程序存在语法错误或可诊断的语义错误”需要符合标准的C ++编译器才能发出诊断信息
彼得-恢复莫妮卡

19
@ Peter-ReinstateMonica好吧,我总是typedef我的结构,而且我当然不会浪费我的粉笔。这绝对是一个品味问题,尽管有极有影响力的人(Torvalds ...)分享您的观点,但像我这样的其他人也会指出,只需要类型的命名约定即可。用struct关键字使代码混乱不堪,这对大写字母(MyClass* object = myClass_create();)无法传达的理解几乎没有帮助。如果您要struct在代码中使用,我会尊重您的。但是我不想要它。
cmaster-恢复莫妮卡

5
就是说,在对C ++进行编程时,确实好习惯是struct仅用于纯旧数据类型以及class任何具有成员函数的类型。但是你不能用用C该公约,因为没有class在C.
cmaster -起用莫妮卡

1
@ Peter-ReinstateMonica是的,嗯,您不能在C中语法附加方法,但这并不意味着Cstruct实际上是POD。在我编写C代码的方式中,大多数结构只能通过单个文件中的代码以及带有其类名的函数来实现。它基本上是没有语法糖的OOP。这使我可以实际控制a内部的哪些变化struct以及在其成员之间保证哪些不变性。因此,我structs倾向于具有成员函数,私有实现,不变式和来自其数据成员的抽象。听起来不像POD,不是吗?
cmaster-恢复莫妮卡

6
只要不禁止使用它们extern "C",我就不会发现此更改有任何问题。没有人应该在C ++中对类型进行定义。这比C ++的语义不同于Java的事实更大。当您学习一种新的编程语言时,您可能需要学习一些新习惯。
科迪·格雷

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,>>也可以成为模板声明的一部分。


嗯,从技术上讲,这是正确的,但是由于使用了>>这种方式,因此该代码在开始时是“非正式的” 。
einpoklum

11

三部曲下降

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

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

对更多的限制 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。

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


三部曲组合不是这个问题的答案-这不是一个沉默的变化。而且,IIANM的第二部分是将实现定义的行为更改为严格的行为,这也不是我要的。
einpoklum

10

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

在C ++ 11之前,即使在复制构造函数具有明显副作用的情况下,在某些情况下也允许但不要求编译器取消复制。现在我们保证了复制省略。行为实质上从实现定义变为必需。

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


1
我以为这个“要求”是在C ++ 17中添加的,而不是C ++ 11中添加的?(请参见临时物化。)
cdhowie

@cdhowie:我认为你是对的。当我写这篇文章时,我手头没有标准,我可能对某些搜索结果过于信任。
阿德里安·麦卡锡

对实现定义的行为进行的更改不会计为该问题的答案。
einpoklum

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时,整数未更改。也就是说,即使在将标准强制回至c ++ 98(带有-std = c ++ 98)的情况下,gcc至少从4.4.7版本开始总是显示出新的行为。

(恕我直言,以前的行为实际上更好:为什么什么都看不到,为什么将值更改为0本身就是有效的?)

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


但是没有提到关于returnType的更改。自C ++ 11以来,仅2个新闻过载可用
成功建立

在C ++ 98和C ++ 11中都定义了这种行为吗?还是行为被定义了?
einpoklum

当cppreference.com是正确的时:“如果发生错误,则v保持不变。(直到C ++ 11)”因此,行为是在C ++ 11之前定义的,并且已更改。
DanRechtsaf

据我了解,确实定义了ss> a的行为,但是对于非常常见的情况,当您读取未初始化的变量时,c ++ 11行为将使用未初始化的变量,这是未定义的行为。因此,故障的默认构造可以防止非常常见的不确定行为。
Rasmus Damgaard Nielsen,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.