What Changes Introduced in C++14 Can Potentially Break a Program Written in C++11

What changes introduced in C++14 can potentially break a program written in C++11?

Note: In this post I consider a "breaking change" to be either, or both, of;

1. a change that will make legal C++11 ill-formed when compiled as C++14, and;

2. a change that will change the runtime behavior when compiled as C++14, vs C++11.


C++11 vs C++14, what does the Standard say?

The Standard draft (n3797) has a section dedicated for just this kind of information, where it describes the (potentially breaking) differences between one revision of the standard, and another.

This post has used that section, [diff.cpp11], as a base for a semi-elaborate discussion regarding the changes that could affect code written for C++11, but compiled as C++14.


C.3.1] Digit Separators

The digit separator was introduced so that one could, in a more readable manner, write numeric literals and split them up in a way that is more natural way.

int x = 10000000;   // (1)
int y = 10'000'000; // (2), C++14

It's easy to see that (2) is much easier to read than (1) in the above snippet, while both initializers have the same value.

The potential issue regarding this feature is that the single-quote always denoted the start/end of a character-literal in C++11, but in C++14 a single-quote can either be surrounding a character-literal, or used in the previously shown manner (2).


Example Snippet, legal in both C++11 and C++14, but with different behavior.

#define M(x, ...) __VA_ARGS__

int a[] = { M(1'2, 3'4, 5) };

// int a[] = { 5 }; <-- C++11
// int a[] = { 3'4, 5 }; <-- C++14
// ^-- semantically equivalent to `{ 34, 5 }`

( Note: More information regarding single-quotes as digit separators can be found in n3781.pdf )


C.3.2] Sized Deallocation

C++14 introduces the opportunity to declare a global overload of operator delete suitable for sized deallocation, something which wasn't possible in C++11.

However, the Standard also mandates that a developer cannot declare just one of the two related functions below, it must declare either none, or both; which is stated in [new.delete.single]p11.

void operator delete (void*) noexcept;
void operator delete (void*, std::size_t) noexcept; // sized deallocation


Further information regarding the potential problem:

Existing programs that redefine the global unsized version do not also
define the sized version. When an implementation introduces a sized
version, the replacement would be incomplete and it is likely that
programs would call the implementation-provided sized deallocator on
objects allocated with the programmer-provided allocator.


Note: Quote taken from n3536 - C++ Sized Deallocation

( Note: More of interest is available in the paper titled n3536 - C++ Sized Deallocation, written by Lawrence Crowl )


C.3.3] constexpr member-functions, no longer implicitly const

There are many changes to constexpr in C++14, but the only change that will change semantics between C++11, and C++14 is the constantness of a member-function marked as constexpr.

The rationale behind this change is to allow constexpr member-functions to mutate the object to which they belong, something which is allowed due to the relaxation of constexpr.

struct A { constexpr int func (); };

// struct A { constexpr int func () const; }; <-- C++11
// struct A { constexpr int func (); }; <-- C++14


Recommended material on this change, and why it is important enough to introduce potential code-breakage:

  • Andrzej's C++ blog - “constexpr” function is not “const”
  • open-std.org - constexpr member functions and implicit const
  • (open-std.org - Relaxing constraints on constexpr functions)


Example snippet, legal in both C++11 and C++14, but with different behavior

struct Obj {
constexpr int func (int) {
return 1;
}

constexpr int func (float) const {
return 2;
}
};

Obj const a = {}; 
int const x = a.func (123);

// int const x = 1; <-- C++11
// int const x = 2; <-- C++14

C.3.4] Removal of std::gets

std::gets has been removed from the Standard Library because it is considered dangerous.

The implications of this is of course that trying to compile code written for C++11, in C++14, where such a function is used will most likely just fail to compile.


( Note: there are ways of writing code that doesn't fail to compile, and have different behavior, that depends on the removal of std::gets from the Standard Library )

Has C++ standard changed with respect to the use of indeterminate values and undefined behavior in C++14?

Yes, this change was driven by changes in the language which makes it undefined behavior if an indeterminate value is produced by an evaluation but with some exceptions for unsigned narrow characters.

Defect report 1787 whose proposed text can be found in N39141 was recently accepted in 2014 and is incorporated in the latest working draft N3936:

The most interesting change with respect to indeterminate values would be to section 8.5 paragraph 12 which goes from:

If no initializer is specified for an object, the object is default-initialized; if no initialization is performed, an object with automatic or dynamic storage duration has indeterminate value. [ Note: Objects with static or thread storage duration are zero-initialized, see 3.6.2. — end note ]

to (emphasis mine):

If no initializer is specified for an object, the object is
default-initialized. When storage for an object with automatic or
dynamic storage duration is obtained, the object has an indeterminate
value
, and if no initialization is performed for the object, that
object retains an indeterminate value until that value is replaced
(5.17 [expr.ass]). [Note: Objects with static or thread storage
duration are zero-initialized, see 3.6.2 [basic.start.init]. —end
note] If an indeterminate value is produced by an evaluation, the
behavior is undefined except in the following cases
:

  • If an indeterminate value of unsigned narrow character type (3.9.1 [basic.fundamental]) is produced by the evaluation of:

    • the second or third operand of a conditional expression (5.16 [expr.cond]),

    • the right operand of a comma (5.18 [expr.comma]),

    • the operand of a cast or conversion to an unsigned narrow character type (4.7 [conv.integral], 5.2.3 [expr.type.conv], 5.2.9
      [expr.static.cast], 5.4 [expr.cast]), or

    • a discarded-value expression (Clause 5 [expr]),


    then the result of the operation is an indeterminate value.

  • If an indeterminate value of unsigned narrow character type (3.9.1 [basic.fundamental]) is produced by the evaluation of the right
    operand of a simple assignment operator (5.17 [expr.ass]) whose first
    operand is an lvalue of unsigned narrow character type, an
    indeterminate value replaces the value of the object referred to by
    the left operand.

  • If an indeterminate value of unsigned narrow character type (3.9.1 [basic.fundamental]) is produced by the evaluation of the
    initialization expression when initializing an object of unsigned
    narrow character type, that object is initialized to an indeterminate
    value.

and included the following example:

[ Example:

int f(bool b) {
unsigned char c;
unsigned char d = c; // OK, d has an indeterminate value
int e = d; // undefined behavior
return b ? d : 0; // undefined behavior if b is true
}

end example ]

We can find this text in N3936 which is the current working draft and N3937 is the C++14 DIS.

Prior to C++1y

It is interesting to note that prior to this draft unlike C which has always had a well specified notion of what uses of indeterminate values were undefined C++ used the term indeterminate value without even defining it (assuming we can not borrow definition from C99) and also see defect report 616. We had to rely on the underspecified lvalue-to-rvalue conversion which in draft C++11 standard is covered in section 4.1 Lvalue-to-rvalue conversion paragraph 1 which says:

[...]if the object is uninitialized, a program that necessitates this conversion has undefined behavior.[...]


Footnotes:

  1. 1787 is a revision of defect report 616, we can find that information in N3903

Can C++ code be valid in both C++03 and C++11 but do different things?

The answer is a definite yes. On the plus side there is:

  • Code that previously implicitly copied objects will now implicitly move them when possible.

On the negative side, several examples are listed in the appendix C of the standard. Even though there are many more negative ones than positive, each one of them is much less likely to occur.

String literals

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

and

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

Type conversions of 0

In C++11, only literals are integer null pointer constants:

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

Rounded results after integer division and modulo

In C++03 the compiler was allowed to either round towards 0 or towards negative infinity. In C++11 it is mandatory to round towards 0

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

Whitespaces between nested template closing braces >> vs > >

Inside a specialization or instantiation the >> might instead be interpreted as a right-shift in C++03. This is more likely to break existing code though: (from 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);
}

Operator new may now throw other exceptions than 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
}

User-declared destructors have an implicit exception specification
example from What breaking changes are introduced in C++11?

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

size() of containers is now required to run in O(1)

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

std::ios_base::failure does not derive directly from std::exception anymore

While the direct base-class is new, std::runtime_error is not. Thus:

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";
}

Is it safe to link C++17, C++14, and C++11 objects

Which combinations of these objects is it and isn't it safe to link into a single binary? Why?

For GCC it is safe to link together any combination of objects A, B, and C. If they are all built with the same version then they are ABI compatible, the standard version (i.e. the -std option) doesn't make any difference.

Why? Because that's an important property of our implementation which we work hard to ensure.

Where you have problems is if you link together objects compiled with different versions of GCC and you have used unstable features from a new C++ standard before GCC's support for that standard is complete. For example, if you compile an object using GCC 4.9 and -std=c++11 and another object with GCC 5 and -std=c++11 you will have problems. The C++11 support was experimental in GCC 4.x, and so there were incompatible changes between the GCC 4.9 and 5 versions of C++11 features. Similarly, if you compile one object with GCC 7 and -std=c++17 and another object with GCC 8 and -std=c++17 you will have problems, because C++17 support in GCC 7 and 8 is still experimental and evolving.

On the other hand, any combination of the following objects will work (although see note below about libstdc++.so version):

  • object D compiled with GCC 4.9 and -std=c++03
  • object E compiled with GCC 5 and -std=c++11
  • object F compiled with GCC 7 and -std=c++17

This is because C++03 support is stable in all three compiler versions used, and so the C++03 components are compatible between all the objects. C++11 support is stable since GCC 5, but object D doesn't use any C++11 features, and objects E and F both use versions where C++11 support is stable. C++17 support is not stable in any of the used compiler versions, but only object F uses C++17 features and so there is no compatibility issue with the other two objects (the only features they share come from C++03 or C++11, and the versions used make those parts OK). If you later wanted to compile a fourth object, G, using GCC 8 and -std=c++17 then you would need to recompile F with the same version (or not link to F) because the C++17 symbols in F and G are incompatible.

The only caveat for the compatibility described above between D, E and F is that your program must use the libstdc++.so shared library from GCC 7 (or later). Because object F was compiled with GCC 7, you need to use the shared library from that release, because compiling any part of the program with GCC 7 might introduce dependencies on symbols that are not present in the libstdc++.so from GCC 4.9 or GCC 5. Similarly, if you linked to object G, built with GCC 8, you would need to use the libstdc++.so from GCC 8 to ensure all symbols needed by G are found. The simple rule is to ensure the shared library the program uses at run-time is at least as new as the version used to compile any of the objects.

Another caveat when using GCC, already mentioned in the comments on your question, is that since GCC 5 there are two implementations of std::string available in libstdc++. The two implementations are not link-compatible (they have different mangled names, so can't be linked together) but can co-exist in the same binary (they have different mangled names, so don't conflict if one object uses std::string and the other uses std::__cxx11::string). If your objects use std::string then usually they should all be compiled with the same string implementation. Compile with -D_GLIBCXX_USE_CXX11_ABI=0 to select the original gcc4-compatible implementation, or -D_GLIBCXX_USE_CXX11_ABI=1 to select the new cxx11 implementation (don't be fooled by the name, it can be used in C++03 too, it's called cxx11 because it conforms to the C++11 requirements). Which implementation is the default depends on how GCC was configured, but the default can always be overridden at compile-time with the macro.

Is C++14 adding new keywords to C++?

Table 4 (Keywords) in N3936 (C++14):

alignas           continue          friend            register          true
alignof decltype goto reinterpret_cast try
asm default if return typedef
auto delete inline short typeid
bool do int signed typename
break double long sizeof union
case dynamic_cast mutable static unsigned
catch else namespace static_assert using
char enum new static_cast virtual
char16_t explicit noexcept struct void
char32_t export nullptr switch volatile
class extern operator template wchar_t
const false private this while
constexpr float protected thread_local
const_cast for public throw

Table 4 in N3337 (C++11):

alignas           continue          friend            register          true
alignof decltype goto reinterpret_cast try
asm default if return typedef
auto delete inline short typeid
bool do int signed typename
break double long sizeof union
case dynamic_cast mutable static unsigned
catch else namespace static_assert using
char enum new static_cast virtual
char16_t explicit noexcept struct void
char32_t export nullptr switch volatile
class extern operator template wchar_t
const false private this while
constexpr float protected thread_local
const_cast for public throw

...which is a long-winded way of saying "no".

(override and final are "identifiers with special meaning" and are listed in Table 3; and etc. are "alternative representations...for certain operators and punctuators" and are listed in Table 5. Neither table changed between C++11 and C++14.)

C++11 backwards compatibility

You are correct. The code itself compiles to work on any platform with a C++ compiler. The new language constructs (ranged for, new keywords like auto, etc.) compile with a new compiler to work just fine on older systems.

However, if you try to link code that uses new symbols (like regex) to an old library, then you run into problems, no matter whether you link statically or dynamically, because the old library does not have the new symbols.

Assuming your code uses new symbols:

  • If you statically link to an old library, the linkage will fail because the new symbols do not exist in the old library.
  • If you dynamically link to an old library, you should still get linker problems, again because the library does not contain the new symbols.
  • If you statically link to a new library, the resulting binary will work on an older system.
  • If you dynamically link to a new library, the resulting binary will work on an older system if it already has the new library or if you distribute the new library with your binary.
    • But if you then try to replace the new dynamic library with an old dynamic library, it will not be able to link to the new symbols in that library, again because they aren't there.

Macro for generalized lambda capture

Actually T.C. is right, the C++11 FDIS says in "16.8 Predefined macro names [cpp.predefined]" that

The name __cplusplus is defined to the value 201103L when compiling a C++ translation unit.

The footnote states:

It is intended that future versions of this standard will replace the value of this macro with a greater value. Non-conforming com- pilers should use a value with at most five decimal digits.

So going with the following code seems totally legit to me.

#if __cplusplus > 201103L
//c++1y or above
#else
//c++11 or below
#endif

However, some compiler might not follow the standard and you might want to check if the _cplusplus value has been incremented for c++1y.

The GCC, for example, had this flag set to 1 until the version 4.7.0.

If you need more information on the _cplusplus flag, you should take a look at this question



Related Topics



Leave a reply



Submit