The first sentence of the proposal:
” The inline
specifier can be applied to variables as well as to functions.
The ¹guaranteed effect of inline
as applied to a function, is to allow the function to be defined identically, with external linkage, in multiple translation units. In practice that means defining the function in a header, that can be included in multiple translation units. The proposal extends this possibility to variables.
So, in practical terms the (now accepted) proposal allows you to use the inline
keyword to define an external linkage const
namespace scope variable, or any static
class data member, in a header file, so that the multiple definitions that result when that header is included in multiple translation units are OK with the linker – it just chooses one of them.
Up until and including C++14 the internal machinery for this has been there, in order to support static
variables in class templates, but there was no convenient way to use that machinery. One had to resort to tricks like
template< class Dummy >
struct Kath_
{
static std::string const hi;
};
template< class Dummy >
std::string const Kath_<Dummy>::hi = "Zzzzz...";
using Kath = Kath_<void>; // Allows you to write `Kath::hi`.
From C++17 and onwards I believe one can write just
struct Kath
{
static std::string const hi;
};
inline std::string const Kath::hi = "Zzzzz..."; // Simpler!
… in a header file.
The proposal includes the wording
” An inline static data member can be defined in the class definition and may specify a brace-or-equal-initializer. If the member is declared with the constexpr
specifier, it may be redeclared in namespace scope with no initializer (this usage is deprecated; see D.X). Declarations of other static data members shall not specify a brace-or-equal-initializer
… which allows the above to be further simplified to just
struct Kath
{
static inline std::string const hi = "Zzzzz..."; // Simplest!
};
… as noted by T.C in a comment to this answer.
Also, the constexpr
specifier implies inline
for static data members as well as functions.
Notes:
¹ For a function `inline` also has a hinting effect about optimization, that the compiler should prefer to replace calls of this function with direct substitution of the function's machine code. This hinting can be ignored.
The compiler is allowed to make one implicit conversion to resolve the parameters to a function. This means that the compiler can use constructors callable with a single parameter to convert from one type to another in order to get the right type for a parameter.
Here's an example with converting constructors that shows how it works:
struct Foo {
// Single parameter constructor, can be used as an implicit conversion.
// Such a constructor is called "converting constructor".
Foo(int x) {}
};
struct Faz {
// Also a converting constructor.
Faz(Foo foo) {}
};
// The parameter is of type Foo, not of type int, so it looks like
// we have to pass a Foo.
void bar(Foo foo);
int main() {
// However, the converting constructor allows us to pass an int.
bar(42);
// Also allowed thanks to the converting constructor.
Foo foo = 42;
// Error! This would require two conversions (int -> Foo -> Faz).
Faz faz = 42;
}
Prefixing the explicit
keyword to the constructor prevents the compiler from using that constructor for implicit conversions. Adding it to the above class will create a compiler error at the function call bar(42)
. It is now necessary to call for conversion explicitly with bar(Foo(42))
The reason you might want to do this is to avoid accidental construction that can hide bugs.
Contrived example:
- You have a
MyString
class with a constructor that constructs a string of the given size. You have a function print(const MyString&)
(as well as an overload print (char *string)
), and you call print(3)
(when you actually intended to call print("3")
). You expect it to print "3", but it prints an empty string of length 3 instead.
Best Answer
Copy elision was permitted to happen under a number of circumstances. However, even if it was permitted, the code still had to be able to work as if the copy were not elided. Namely, there had to be an accessible copy and/or move constructor.
Guaranteed copy elision redefines a number of C++ concepts, such that certain circumstances where copies/moves could be elided don't actually provoke a copy/move at all. The compiler isn't eliding a copy; the standard says that no such copying could ever happen.
Consider this function:
Under non-guaranteed copy elision rules, this will create a temporary, then move from that temporary into the function's return value. That move operation may be elided, but
T
must still have an accessible move constructor even if it is never used.Similarly:
This is copy initialization of
t
. This will copy initializet
with the return value ofFunc
. However,T
still has to have a move constructor, even though it will not be called.Guaranteed copy elision redefines the meaning of a prvalue expression. Pre-C++17, prvalues are temporary objects. In C++17, a prvalue expression is merely something which can materialize a temporary, but it isn't a temporary yet.
If you use a prvalue to initialize an object of the prvalue's type, then no temporary is materialized. When you do
return T();
, this initializes the return value of the function via a prvalue. Since that function returnsT
, no temporary is created; the initialization of the prvalue simply directly initilaizes the return value.The thing to understand is that, since the return value is a prvalue, it is not an object yet. It is merely an initializer for an object, just like
T()
is.When you do
T t = Func();
, the prvalue of the return value directly initializes the objectt
; there is no "create a temporary and copy/move" stage. SinceFunc()
's return value is a prvalue equivalent toT()
,t
is directly initialized byT()
, exactly as if you had doneT t = T()
.If a prvalue is used in any other way, the prvalue will materialize a temporary object, which will be used in that expression (or discarded if there is no expression). So if you did
const T &rt = Func();
, the prvalue would materialize a temporary (usingT()
as the initializer), whose reference would be stored inrt
, along with the usual temporary lifetime extension stuff.One thing guaranteed elision permits you to do is return objects which are immobile. For example,
lock_guard
cannot be copied or moved, so you couldn't have a function that returned it by value. But with guaranteed copy elision, you can.Guaranteed elision also works with direct initialization:
If
FactoryFunction
returnsT
by value, this expression will not copy the return value into the allocated memory. It will instead allocate memory and use the allocated memory as the return value memory for the function call directly.So factory functions that return by value can directly initialize heap allocated memory without even knowing about it. So long as these function internally follow the rules of guaranteed copy elision, of course. They have to return a prvalue of type
T
.Of course, this works too:
In case you don't like writing typenames.
It is important to recognize that the above guarantees only work for prvalues. That is, you get no guarantee when returning a named variable:
In this instance,
t
must still have an accessible copy/move constructor. Yes, the compiler can choose to optimize away the copy/move. But the compiler must still verify the existence of an accessible copy/move constructor.So nothing changes for named return value optimization (NRVO).