template
typename... Ts> // types of arguments to use
void doSomeWork(Ts&&... params) {
create local T object from params ...
…
}
There are two ways to turn the line of pseudocode into real code (see Item 25for information about std::forward
):
T localObject (std::forward(params)... ); // using parens
T localObject {std::forward(params)... }; // using braces
So consider this calling code:
std::vector v;
…
doSomeWork>(10, 20);
If doSomeWork
uses parentheses when creating localObject
, the result is a std::vector
with 10 elements. If doSomeWork
uses braces, the result is a std::vector
with 2 elements. Which is correct? The author of doSomeWork
can't know. Only the caller can.
This is precisely the problem faced by the Standard Library functions std::make_unique
and std::make_shared
(see Item 21). These functions resolve the problem by internally using parentheses and by documenting this decision as part of their interfaces. [1] More flexible designs — ones that permit callers to determine whether parentheses or braces should be used in functions generated from a template — are possible. For details, see the 5 June 2013 entry of Andrzej's C++ blog , “ Intuitive interface — Part I . ”
Things to Remember
• Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it's immune to C++'s most vexing parse.
• During constructor overload resolution, braced initializers are matched to std::initializer_list
parameters if at all possible, even if other constructors offer seemingly better matches.
• An example of where the choice between parentheses and braces can make a significant difference is creating a std::vector< numeric type >
with two arguments.
• Choosing between parentheses and braces for object creation inside templates can be challenging.
Item 8: Prefer nullptr
to 0
and NULL
.
So here's the deal: the literal 0
is an int
, not a pointer. If C++ finds itself looking at 0
in a context where only a pointer can be used, it'll grudgingly interpret 0
as a null pointer, but that's a fallback position. C++'s primary policy is that 0
is an int
, not a pointer.
Practically speaking, the same is true of NULL
. There is some uncertainty in the details in NULL
's case, because implementations are allowed to give NULL
an integral type other than int
(e.g., long
). That's not common, but it doesn't really matter, because the issue here isn't the exact type of NULL
, it's that neither 0
nor NULL
has a pointer type.
In C++98, the primary implication of this was that overloading on pointer and integral types could lead to surprises. Passing 0
or NULL
to such overloads never called a pointer overload:
void f(int); // three overloads of f
void f(bool);
void f(void*);
f(0); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls
// f(int). Never calls f(void*)
The uncertainty regarding the behavior of f(NULL)
is a reflection of the leeway granted to implementations regarding the type of NULL
. If NULL
is defined to be, say, 0L
(i.e., 0
as a long
), the call is ambiguous, because conversion from long
to int
, long
to bool,
and 0L
to void*
are considered equally good. The interesting thing about that call is the contradiction between the apparent meaning of the source code (“I'm calling f
with NULL
— the null pointer”) and its actual meaning (“I'm calling f
with some kind of integer — not the null pointer”). This counterintuitive behavior is what led to the guideline for C++98 programmers to avoid overloading on pointer and integral types. That guideline remains valid in C++11, because, the advice of this Item notwithstanding, it's likely that some developers will continue to use 0
and NULL
, even though nullptr
is a better choice.
nullptr
's advantage is that it doesn't have an integral type. To be honest, it doesn't have a pointer type, either, but you can think of it as a pointer of all types. nullptr
's actual type is std::nullptr_t
, and, in a wonderfully circular definition, std::nullptr_t
is defined to be the type of nullptr
. The type std::nullptr_t
implicitly converts to all raw pointer types, and that's what makes nullptr
act as if it were a pointer of all types.
Calling the overloaded function f
with nullptr
calls the void*
overload (i.e., the pointer overload), because nullptr
can't be viewed as anything integral:
f(nullptr); // calls f(void*) overload
Using nullptr
instead of 0
or NULL
thus avoids overload resolution surprises, but that's not its only advantage. It can also improve code clarity, especially when auto
variables are involved. For example, suppose you encounter this in a code base:
auto result = findRecord( /* arguments */ );
if ( result == 0) {
…
}
If you don't happen to know (or can't easily find out) what findRecord
returns, it may not be clear whether result
is a pointer type or an integral type. After all, 0
(what result
is tested against) could go either way. If you see the following, on the other hand,
auto result = findRecord( /* arguments */ );
if ( result == nullptr) {
…
}
there's no ambiguity: result
must be a pointer type.
nullptr
shines especially brightly when templates enter the picture. Suppose you have some functions that should be called only when the appropriate mutex has been locked. Each function takes a different kind of pointer:
int f1(std::shared_ptr spw); // call these only when
double f2(std::unique_ptr upw); // the appropriate
bool f3(Widget* pw); // mutex is locked
Calling code that wants to pass null pointers could look like this:
std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxGuard = // C++11 typedef; see Item 9
std::lock_guard;
…
{
MuxGuard g(f1m); // lock mutex for f1
auto result = f1( 0); // pass 0 as null ptr to f1
} // unlock mutex
…
{
MuxGuard g(f2m); // lock mutex for f2
auto result = f2( NULL); // pass NULL as null ptr to f2
} // unlock mutex
…
{
Читать дальше