C++代码能在C++03和C++11中都是有效的,但会有不同的行为吗?

302

是否可以将C++代码符合C++03标准和C++11标准,但根据编译使用的标准而执行不同的操作?


27
我很确定 auto 可能会导致这样的情况。 - OMGtechy
9
可以。一个例子是在模板中使用 >> 符号。你可以构造一个情境,使其能够符合两个标准的编译要求。另一个很容易找到变化的例子是在初始化方面。 - chris
6
这是一篇关于“>>”符号情况的好文章:http://gustedt.wordpress.com/2013/12/15/a-disimprovement-observed-from-the-outside-right-angle-brackets/ - chris
6
@OMGtechy说:“我不认为auto会引起这种情况。在旧的含义下,auto声明需要一个类型名称;而在新的含义下,不允许使用类型名称。” - Keith Thompson
2
它为什么是开放式的?你自己通过另一个问题指出了这个问题的答案是“是的,这里有一个例子”。正如你自己所指出的那样,这个问题有一个非常明确的答案。 - jalf
显示剩余15条评论
7个回答

287

答案是肯定的。好处在于:

  • 以前隐式复制对象的代码现在将在可能的情况下隐式移动它们。

不利方面,在标准的附录C中列出了几个例子。尽管负面的例子比正面的多得多,但每一个负面例子发生的可能性都要小得多。

字符串字面值

#define u8 "abc"
const char* s = u8"def"; // Previously "abcdef", now "def"

#define _x "there"
"hello "_x // Previously "hello there", now a user defined string literal

0的类型转换

在C++11中,只有字面值可以作为整数空指针常量:

void f(void *); // #1
void f(...); // #2
template<int N> void g() {
    f(0*N); // Calls #2; used to call #1
}

整数除法和取余后的结果进行四舍五入

C++03编译器允许向0或负无穷方向进行四舍五入。在C++11中,强制必须向0进行四舍五入。

int i = (-1) / 2; // Might have been -1 in C++03, is now ensured to be 0

嵌套模板结束大括号之间的空格 >> vs > >

在特化或实例化的内部,>> 可能会被解释为 C++03 中的右移。然而,这更有可能破坏现有代码:(来自http://gustedt.wordpress.com/2013/12/15/a-disimprovement-observed-from-the-outside-right-angle-brackets/)

template< unsigned len > unsigned int fun(unsigned int x);
typedef unsigned int (*fun_t)(unsigned int);
template< fun_t f > unsigned int fon(unsigned int x);

void total(void) {
    // fon<fun<9> >(1) >> 2 in both standards
    unsigned int A = fon< fun< 9 > >(1) >>(2);
    // fon<fun<4> >(2) in C++03
    // Compile time error in C++11
    unsigned int B = fon< fun< 9 >>(1) > >(2);
}

操作符new现在可能会抛出除std::bad_alloc之外的其他异常

struct foo { void *operator new(size_t x){ throw std::exception(); } }
try {
    foo *f = new foo();
} catch (std::bad_alloc &) {
    // c++03 code
} catch (std::exception &) {
    // c++11 code
}

用户声明的析构函数具有隐含的异常规范 C++11中引入的哪些破坏性变化?举例说明。

struct A {
    ~A() { throw "foo"; } // Calls std::terminate in C++11
};
//...
try { 
    A a; 
} catch(...) { 
    // C++03 will catch the exception
} 

size()方法现在要求容器的时间复杂度为O(1)

std::list<double> list;
// ...
size_t s = list.size(); // Might be an O(n) operation in C++03

std::ios_base::failure不再直接派生自std::exception

虽然直接基类是新的,但std::runtime_error不是。因此:

try {
    std::cin >> variable; // exceptions enabled, and error here
} catch(std::runtime_error &) {
    std::cerr << "C++11\n";
} catch(std::ios_base::failure &) {
    std::cerr << "Pre-C++11\n";
}

11
好的,+1。另一个是,现在声明析构函数的用户会隐式地具有 noexecpt(true) 特性,因此在析构函数中抛出异常将会调用 std::terminate。但我希望编写这种代码的人会对此感到满意! - typ1232
4
但是 std::system_error 本身(间接地)派生自 std::exception,所以 catch (std::exception &) 仍然可以捕获 std::ios_base::failure。 - user2665887
5
我很困惑,因为你所说的关于 operator new 的内容是准确的(它现在可以抛出 std::bad_array_new_length 异常),但你展示的例子并没有显示出来。据我所知,你展示的代码在 C++03 和 C++11 中是相同的。 - Mooing Duck
2
list::size的优点是O(1),但缺点是splice现在变成了O(n)。 - Tony Delroy
问题:在C++11之前,是否有任何编译器将除法向负无穷取整? - user541686
显示剩余3条评论

56

我指向这篇文章其后续文章,其中有一个很好的例子显示如何在C++03和C++11中仍能编译的情况下,>>的含义是如何从C++03转变为C++11的。

bool const one = true;
int const two = 2;
int const three = 3;

template<int> struct fun {
    typedef int two;
};

template<class T> struct fon {
    static int const three = ::three;
    static bool const one = ::one;
};

int main(void) {
    fon< fun< 1 >>::three >::two >::one; // valid for both  
}

关键部分是在main中的一行表达式。

C++03版本:

1 >> ::three = 0
=> fon< fun< 0 >::two >::one;

fun< 0 >::two = int
=> fon< int >::one

fon< int >::one = true
=> true

在C++11中

fun< 1 > is a type argument to fon
fon< fun<1> >::three = 3
=> 3 > ::two > ::one

::two is 2 and ::one is 1
=> 3 > 2 > 1
=> (3 > 2) > 1
=> true > 1
=> 1 > 1
=> false
恭喜,相同的表达式得出了两个不同的结果。虽然当我测试时,C++03的那个会在Clang中产生一个警告。
恭喜,相同的表达式产生了两个不同的结果。尽管我测试时发现 C++03 版本在 Clang 中会产生警告。

在C++03版本中,对于::two不需要使用typename这一点很奇怪。 - zahir
3
不错,把它简化为不同标准的“真”或“假”,也许我们可以将其用作功能测试</玩笑>。 - cmaster - reinstate monica
@zahir,这不是一种类型,而只是一个值。 - chris
例如,令人惊讶的是,GCC会发出警告,但Clang不会。 - chris
@chris 确实很奇怪。通常clang会显示更多的警告,我认为像这样编写的代码是有问题的,因此应该给出警告。 - example
显示剩余3条评论

39
是的,C++03和C++11之间有许多变化会导致相同的代码产生不同的行为。其中序列规则差异会带来一些有趣的变化,包括一些先前未定义的行为变得定义明确。
1. 在初始化列表中多次改变同一变量的变异
一个非常有趣的特殊情况是在初始化列表中多次改变同一变量的变异,例如:
int main()
{
    int count = 0 ;
    int arrInt[2] = { count++, count++ } ;

    return 0 ;
}

在C++03和C++11中,这是良好定义的,但是在C++03中,评估顺序是未指定的,而在C++11中,它们按照出现的顺序进行评估。因此,如果我们使用clang以C++03模式编译,它会提供以下警告(请查看实时演示):

warning: multiple unsequenced modifications to 'count' [-Wunsequenced]

    int arrInt[2] = { count++, count++ } ;

                           ^        ~~

但是在C++11中没有提供警告(请查看实况)。

2. C++11的新排序规则使得 i = ++ i + 1; 的行为被定义。

C++03之后采用的新排序规则意味着:

int i = 0 ;
i = ++ i + 1;

C++11中已经不再将其视为未定义行为,这在缺陷报告637.顺序规则和示例的不一致中有所涵盖。

3. C++11引入的新顺序规则也使++++i;的行为定义明确了。

C++03之后采用的新顺序规则意味着:

int i = 0 ;
++++i ;

C++11中,is no longer undefined behavior

4. 稍微更合理的有符号左移

C++11后续的草案中包括 N3485,我在下面提供链接fixed the undefined behavior of shifting a 1 bit into or past the sign bit。这也涵盖在defect report 1457中。Howard Hinnant在Is left-shifting (<<) a negative integer undefined behavior in C++11?线程中评论了这个变化的重要性。

5. constexpr函数可以在C++11中被视为编译时常量表达式

C++11引入了constexpr函数,它们:

关键字声明可以在编译时计算函数或变量的值。这样的变量和函数可以在只允许编译时常量表达式的地方使用。

虽然C++03没有constexpr特性,但是我们不必显式地使用constexpr关键字,因为标准库提供了许多在C++11中作为constexpr的函数。例如std::numeric_limits::min。这可能会导致不同的行为,例如:

#include <limits>

int main()
{
    int x[std::numeric_limits<unsigned int>::min()+2] ;
}

在C++03中使用clang将导致x成为变长数组,这是一种扩展,并将生成以下警告:

warning: variable length arrays are a C99 feature [-Wvla-extension]
    int x[std::numeric_limits<unsigned int>::min()+2] ;
         ^

在C++11中,std::numeric_limits<unsigned int>::min()+2是一个编译时常量表达式,不需要VLA扩展。

6. 在C++11中,析构函数会隐式生成noexcept异常规格。

由于在C++11中,用户定义的析构函数具有隐式的noexcept(true)规格,如noexcept destructors所述,这意味着以下程序:

#include <iostream>
#include <stdexcept>

struct S
{
  ~S() { throw std::runtime_error(""); } // bad, but acceptable
};

int main()
{
  try { S s; }
  catch (...) {
    std::cerr << "exception occurred";
  } 
 std::cout << "success";
}

在C++11中会调用std::terminate,但在C++03中可以成功运行。
在C++03中,模板参数不能具有内部链接性。
这在为什么std::sort不接受在函数内声明的Compare类中很好地解释了。因此,以下代码在C++03中不应该工作:
#include <iostream>
#include <vector>
#include <algorithm>

class Comparators
{
public:
    bool operator()(int first, int second)
    {
        return first < second;
    }
};

int main()
{
    class ComparatorsInner : public Comparators{};

    std::vector<int> compares ;
    compares.push_back(20) ;
    compares.push_back(10) ;
    compares.push_back(30) ;

    ComparatorsInner comparatorInner;
    std::sort(compares.begin(), compares.end(), comparatorInner);

    std::vector<int>::iterator it;
    for(it = compares.begin(); it != compares.end(); ++it)
    {
        std::cout << (*it) << std::endl;
    }
}

目前在C++03模式下,clang允许使用这段代码并发出警告,除非您使用-pedantic-errors标志,这有点棘手,请参见在线演示

8. >>在关闭多个模板时不再是非法的

使用>>来关闭多个模板不再是非法的,但可能会导致在C++03和C+11中产生不同结果的代码。以下示例摘自右角括号和向后兼容性

#include <iostream>
template<int I> struct X {
  static int const c = 2;
};
template<> struct X<0> {
  typedef int c;
};
template<typename T> struct Y {
  static int const c = 3;
};
static int const c = 4;
int main() {
  std::cout << (Y<X<1> >::c >::c>::c) << '\n';
  std::cout << (Y<X< 1>>::c >::c>::c) << '\n';
}

而在C++03中的结果是:

0
3

而在C++11中:

0
0

9. C++11 改变了一些 std::vector 构造函数

稍微修改 这个答案 中的代码,可以看到使用以下来自 std::vector 的构造函数:

std::vector<T> test(1);

在C++03和C++11中会产生不同的结果:
#include <iostream>
#include <vector>

struct T
{
    bool flag;
    T() : flag(false) {}
    T(const T&) : flag(true) {}
};


int main()
{
    std::vector<T> test(1);
    bool is_cpp11 = !test[0].flag;

    std::cout << is_cpp11 << std::endl ;
}

在C++11中,聚合初始化器中的缩小转换是不合法的。看起来,gcc在C++11和C++03中都允许这种情况,但在C++11中默认提供警告:
int x[] = { 2.0 };

这在草案 C++11 标准的第8.5.4节“列表初始化”第3段中有所涉及:

类型为 T 的对象或引用的列表初始化定义如下:

其中包含以下重点(强调我的):

否则,如果 T 是类类型,则考虑构造函数。逐个枚举可适用的构造函数,并通过重载决议(13.3、13.3.1.7)选择最佳构造函数。如果需要进行任何参数的缩小转换(请参阅下文),则程序将是非法的

此外,还有许多类似的实例在草案C++标准的附录C.2“C++和ISO C++ 2003”中有所涉及。它还包括:
  • New kinds of string literals [...] Specifically, macros named R, u8, u8R, u, uR, U, UR, or LR will not be expanded when adjacent to a string literal but will be interpreted as part of the string literal. For example

    #define u8 "abc"
    const char *s = u8"def"; // Previously "abcdef", now "def"
    
  • User-defined literal string support [...]Previously, #1 would have consisted of two separate preprocessing tokens and the macro _x would have been expanded. In this International Standard, #1 consists of a single preprocessing tokens, so the macro is not expanded.

    #define _x "there"
    "hello"_x // #1
    
  • Specify rounding for results of integer / and % [...] 2003 code that uses integer division rounds the result toward 0 or toward negative infinity, whereas this International Standard always rounds the result toward 0.

  • Complexity of size() member functions now constant [...] Some container implementations that conform to C++ 2003 may not conform to the specified size() requirements in this International Standard. Adjusting containers such as std::list to the stricter requirements may require incompatible changes.

  • Change base class of std::ios_base::failure [...] std::ios_base::failure is no longer derived directly from std::exception, but is now derived from std::system_error, which in turn is derived from std::runtime_error. Valid C++ 2003 code that assumes that std::ios_base::failure is derived directly from std::exception may execute differently in this International Standard.


那么大多数的例子都可以归结为以前未定义的行为现在已经被明确定义了吗? - MatthiasB
@MatthiasB 2、3和4就是这个问题,所以在这一点上它们不再是大多数示例了。我怀疑我不会再找到更多的未定义行为示例,因此随着我添加更多内容,它们将成为更小的集合。 - Shafik Yaghmour
好的,#1行为是未指定的,所以我会将其视为未定义的行为(至少在c++03中不能期望获得特定结果,现在在c++11中可以)。#5使用了c++的非标准扩展。但我想你是对的。你越找,就会发现越多的例子在两个标准中都有定义,但产生不同的结果。 - MatthiasB
@MatthiasB 是的,未指定行为和未定义行为都会产生不良结果。至于考虑 Linux 的扩展 取决于许多 GCC 扩展 ,我们应该假设在现实世界中它们很重要。当我第一次回答这个问题时,我没有预料到会找到这么多例子。 - Shafik Yaghmour

35

一个可能有危险的不向后兼容的更改出现在序列容器的构造函数中,例如std::vector,特别是在指定初始大小的重载中。在C++03中,它们复制了一个默认构造的元素,而在C++11中,它们会默认构造每个元素。

考虑以下示例(使用boost::shared_ptr,以便在C++03中有效):

#include <deque>
#include <iostream>

#include "boost/shared_ptr.hpp"


struct Widget
{
  boost::shared_ptr<int> p;

  Widget() : p(new int(42)) {}
};


int main()
{
  std::deque<Widget> d(10);
  for (size_t i = 0; i < d.size(); ++i)
    std::cout << "d[" << i << "] : " << d[i].p.use_count() << '\n';
}

C++03现场示例

C++11现场示例

原因是C++03为"指定大小和原型元素"和"仅指定大小"指定了一个重载,如下所示(为简洁起见省略了分配器参数):

container(size_type size, const value_type &prototype = value_type());

这将始终将 prototype 复制到容器中 size 次。当只使用一个参数调用时,它将创建 size 个默认构造元素的副本。

在 C++11 中,这个构造函数的签名被移除并替换为下面这两种重载形式:

container(size_type size);

container(size_type size, const value_type &prototype);

第二个函数与以前相同,创建prototype元素的size个副本。然而,第一个函数(现在只处理指定大小参数的调用)会分别进行默认构造每个元素。

我猜测这种变化的原因是C++03重载无法与仅移动元素类型一起使用。但它仍然是一项破坏性的变化,并且很少有文档记录。


3
虽然这显然是一个重大的变化,但我更喜欢C++11的行为。我期望这将导致deque容纳十个独立的小部件,而不是共享同一资源的十个小部件。 - Agentlien

20
std::istream读取失败的结果已经改变。CppReference很好地总结了这一点:
如果提取失败(例如,在期望数字时输入字母),则保持value不变并设置failbit标志。(C++11之前) 如果提取失败,则将零写入value并设置failbit标志。如果提取结果太大或太小而无法适应value,则会写入std::numeric_limits<T> :: max()std::numeric_limits<T> :: min()并设置failbit标志。(自C++11以来) 如果您习惯于新语义,然后必须使用C++03编写,则主要是一个问题。以下做法并不特别好,但在C++11中定义明确:
int x, y;
std::cin >> x >> y;
std::cout << x + y;

然而,在C++03中,上述代码使用了未初始化的变量,因此具有未定义的行为。

4
您可以补充一下,在C++03中,可以利用这种标准化的行为来提供默认值,例如 int x = 1, y = 1; cin >> x >> y; cout << x*y;。使用C++03时,当无法读取到y值时,将正确地生成x值。 - cmaster - reinstate monica

16

这个线程"在运行时,C++03和C++0x之间有哪些差异可以检测到"提供了一些例子(从那个线程复制而来),通过利用C++11引用折叠等方式确定语言差异:

template <class T> bool f(T&) {return true; } 
template <class T> bool f(...){return false;} 

bool isCpp11() 
{
    int v = 1;
    return f<int&>(v); 
}

同时C++11允许将局部类型用作模板参数:

template <class T> bool cpp11(T)  {return true;} //T cannot be a local type in C++03
                   bool cpp11(...){return false;}

bool isCpp0x() 
{
   struct local {} var; //variable with local type
   return cpp11(var);
}

7

以下是另一个例子:

#include <iostream>

template<class T>
struct has {
  typedef char yes;
  typedef yes (&no)[2];    
  template<int> struct foo;    
  template<class U> static yes test(foo<U::bar>*);      
  template<class U> static no  test(...);    
  static bool const value = sizeof(test<T>(0)) == sizeof(yes);
};

enum foo { bar };

int main()
{
    std::cout << (has<foo>::value ? "yes" : "no") << std::endl;
}

输出:

Using c++03: no
Using c++11: yes

See the result on Coliru


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接