Why should I prefer the "explicitly typed initializer" idiom over explicitly giving the type
Asked Answered
O

4

46

I've recently bought the new Effective modern C++ from Scott Meyers and reading through it now. But I have encountered one thing which totally bugs me.

In item 5, Scott says that using auto is a great thing. It saves typing, gives you in most cases the correct type and it might be immune to type mismatches. I totally understand this and think of auto as a good thing too.

But then in item 6, Scott says that every coin has two sides. Likewise, there might be cases when auto deduces a totally wrong type, e.g. for proxy objects.

You may already know this example:

class Widget;
std::vector<bool> features(Widget w);

Widget w;

bool priority = features(w)[5]; // this is fine

auto priority = features(w)[5]; // this result in priority being a proxy
                                // to a temporary object, which will result
                                // in undefined behavior on usage after that
                                // line

So far, so good.

But Scott's solution to this, is the so called "explicitly typed initializer idiom". The idea is, to use static_cast on the initializer like this:

auto priority = static_cast<bool>(features(w)[5]);

But this not only leads to more typing, but means you also explicitly state the type, which should be deduced. You basically lose both advantages of auto over an explicit given type.

Can anyone tell me, why it is advantageous to use this idiom?


First to clear things up, my questions aims to why I should write:

auto priority = static_cast<bool>(features(w)[5]);

instead of:

bool priority = features(w)[5];

@Sergey brought up a link to a nice article on GotW about this topic, which partly answers my question.

Guideline: Consider declaring local variables auto x = type{ expr }; when you do want to explicitly commit to a type. It is self-documenting to show that the code is explicitly requesting a conversion, it guarantees the variable will be initialized, and it won’t allow an accidental implicit narrowing conversion. Only when you do want explicit narrowing, use ( ) instead of { }.

Which basically brings me to a related question. Which of these four alternatives should I choose?

bool priority = features(w)[5];

auto priority = static_cast<bool>(features(w)[5]);

auto priority = bool(features(w)[5]);

auto priority = bool{features(w)[5]};

Number one is still my favorite. It's less typing and as explicit as the three other ones.

The point about the guaranteed initialization doesn't really hold, as I'm declaring variables anyways not before I can initialize them somehow. And the other argument about the narrowing didn't work out well in a quick test.

Objection answered 1/9, 2014 at 13:45 Comment(8)
I read his books (not this one), and I doubt he didn't explain.Nika
@Niall: From what I understand the question is not why this is needed, but why put the type in static_cast rather than in place of autoDenverdeny
@PiotrS. exactly that's my point.Objection
@BЈовић unfortunately not. He added a side note for another case, where you intentionally have to cast from double to int.Objection
@PiotrS. Why put the type in static_cast is explained in the comment (i.e., to avoid UB). I think that what the author is trying to say is to use his solution only to specific situations as the illustrated example. There's no need to use it when use of auto doesn't cause any implications.Clasp
@40two: nah... the question is why Scott has invented an idiom that looks: auto x = static_cast<Y>(z); for what can be expressed as Y x = z;Denverdeny
And, Y x=e; is better because auto x=static_cast<Y>(e); activates explicit conversions, which should be used with caution. I suspect annotation saying the return value should not be persisted past the invoked line might be in order for C++.Selfwinding
About your test for narrowing: that code should not compile. The compiler you checked is not correctly implementing brace initialization. Presumably it will get fixed. rextester.com/SFTL92198Farmyard
D
27

Following the C++ Standard:

§ 8.5 Initializers [dcl.init]

  1. The initialization that occurs in the form

    T x = a;
    

    as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization.

I can think of the example given in the book:

auto x = features(w)[5];

as the one that represents any form of copy-initialization with auto / template type (deduced type in general), just like:

template <typename A>
void foo(A x) {}

foo(features(w)[5]);

as well as:

auto bar()
{
    return features(w)[5];
}

as well as:

auto lambda = [] (auto x) {};
lambda(features(w)[5]);

So the point is, we cannot always just "move type T from static_cast<T> to the left-hand side of assignment".

Instead, in any of the above examples we need to explicitly specify the desired type rather than allowing compiler deduce one on its own, if the latter can lead to undefined behavior:

Respectively to my examples that would be:

/*1*/ foo(static_cast<bool>(features(w)[5]));

/*2*/ return static_cast<bool>(features(w)[5]);

/*3*/ lambda(static_cast<bool>(features(w)[5]));

As such, using static_cast<T> is an elegant way of forcing a desired type, which alternatively can be expressed by explicit contructor call:

foo(bool{features(w)[5]});

To summarize, I don't think the book says:

Whenever you want to force the type of a variable, use auto x = static_cast<T>(y); instead of T x{y};.

To me it sounds more like a word of warning:

The type inference with auto is cool, but may end up with undefined behavior if used unwisely.

And as a solution for the scenarios involving type deduction, the following is proposed:

If the compiler's regular type-deduction mechanism is not what you want, use static_cast<T>(y).


UPDATE

And answering your updated question, which of the below initializations should one prefer:

bool priority = features(w)[5];

auto priority = static_cast<bool>(features(w)[5]);

auto priority = bool(features(w)[5]);

auto priority = bool{features(w)[5]};

Scenario 1

First, imagine the std::vector<bool>::reference is not implicitly convertible to bool:

struct BoolReference
{
    explicit operator bool() { /*...*/ }
};

Now, the bool priority = features(w)[5]; will not compile, as it is not an explicit boolean context. The others will work fine (as long as the operator bool() is accessible).

Scenario 2

Secondly, let's assume the std::vector<bool>::reference is implemented in an old fashion, and although the conversion operator is not explicit, it returns int instead:

struct BoolReference
{
    operator int() { /*...*/ }
};

The change in signature turns off the auto priority = bool{features(w)[5]}; initialization, as using {} prevents the narrowing (which converting an int to bool is).

Scenario 3

Thirdly, what if we were talking not about bool at all, but about some user-defined type, that, to our surprise, declares explicit constructor:

struct MyBool
{
    explicit MyBool(bool b) {}
};

Surprisingly, once again the MyBool priority = features(w)[5]; initialization will not compile, as the copy-initialization syntax requires non-explicit constructor. Others will work though.

Personal attitude

If I were to choose one initialization from the listed four candidates, I would go with:

auto priority = bool{features(w)[5]};

because it introduces an explicit boolean context (which is fine in case we want to assign this value to boolean variable) and prevents narrowing (in case of other types, not-easily-convertible-to-bool), so that when an error/warning is triggered, we can diagnose what features(w)[5] really is.


UPDATE 2

I have recently watched Herb Sutter's speech from CppCon 2014 titled Back to the Basics! Essentials of Modern C++ Style, where he presents some points on why should one prefer the explicit type initializer of auto x = T{y}; form (though it is not the same as with auto x = static_cast<T>(y), so not all arguments apply) over T x{y};, which are:

  1. auto variables must always be initialized. That is, you can't write auto a;, just like you can write error-prone int a;

  2. The modern C++ style prefers the type on the right side, just like in:

    a) Literals:

    auto f = 3.14f;
    //           ^ float
    

    b) User-defined literals:

    auto s = "foo"s;
    //            ^ std::string
    

    c) Function declarations:

    auto func(double) -> int;
    

    d) Named lambdas:

    auto func = [=] (double) {};
    

    e) Aliases:

    using dict = set<string>;
    

    f) Template aliases:

    template <class T>
    using myvec = vector<T, myalloc>;
    

    so as such, adding one more:

    auto x = T{y};
    

    is consistent with the style where we have name on the left side, and type with initializer on the right side, what can be briefly described as:

    <category> name = <type> <initializer>;
    
  3. With copy-elision and non-explicit copy/move constructors it has zero-cost compared to T x{y} syntax.

  4. It is more explicit when there are subtle differences between the types:

     unique_ptr<Base> p = make_unique<Derived>(); // subtle difference
    
     auto p = unique_ptr<Base>{make_unique<Derived>()}; // explicit and clear
    
  5. {} guarantees no implicit conversions and no narrowing.

But he also mentions some drawbacks of the auto x = T{} form in general, which has already been described in this post:

  1. Even though the compiler can elide the right-hand side's temporary, it requires an accessible, non-deleted and non-explicit copy-constructor:

     auto x = std::atomic<int>{}; // fails to compile, copy constructor deleted
    
  2. If the elision is not enabled (e.g. -fno-elide-constructors), then moving non-movable types results in expensive copy:

     auto a = std::array<int,50>{};
    
Denverdeny answered 1/9, 2014 at 16:18 Comment(7)
Thanks for your input, in these cases, I see some benefits for using the static_cast, but I don't see, why it is related to my original question? I mean, just because there is also an auto somewhere doesn't mean, you have to obey the same rules.Objection
@Mario: because the same rules of deducing the type are applied.Denverdeny
@Mario: I don't think the book says: "Whenever you want to force the type of a variable, use auto x = static_cast<T>(y); instead of T x{y};". To me it looks as just a hint the book gives how to force the desired type, skipping the regular type-deduction mechanism.Denverdeny
I'm still not convinced, that it's a good idea for the given code, but I see your point for the other cases. This might be the thing, that Scott had in his mind.Objection
It seems pointless to compare these four auto x = y as T cases and avoid T x{y};...Permanence
It is not pointless if we talk about copy-initialization syntax in general (see my first paragraphs), not simple assignment like auto x = z;, but e.g. template argument deduction that follows the same rules as auto.Denverdeny
I think it would be nice to update your answer to include guaranteed copy elision (open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0135r0.html) which is now a part of C++17, enabling the use of auto more frequently.Zachary
P
15

I don't have the book in front of me, so I can't tell if there's more context.

But to answer your question, no, using auto+static_cast in this particular example is not a good solution. It violates another guideline (one for which I've never seen any exceptions justified):

  • Use the weakest cast/conversion possible.

Unnecessarily strong casts subvert the type system and prevent the compiler from generating diagnostic messages in case a change occurs elsewhere in the program that affects the conversion in an incompatible way. (action at a distance, the boogey-man of maintenance programming)

Here the static_cast is unnecessarily strong. An implicit conversion will do just fine. So avoid the cast.

Permanence answered 1/9, 2014 at 14:43 Comment(2)
I really like your answer, but I've got the feeling, that @Piotr S. 's answer is more in the direction, on how it is meant in the book.Objection
@Mario: The explicit constructor call T var{expression} is indeed better, because it doesn't defeat the type system as strongly as static_cast. And, where possible, T var = expression; may be preferred because it's even weaker, although unnecessarily permitting explicit constructor invocation is arguably a lot less problematic than the introduction of a cast.Permanence
F
8

Context from the book:

Though std::vector<bool> conceptually holds bools, operator[] for std::vector<bool> doesn’t return a reference to an element of the container (which is what std::vector::operator[] returns for every type except bool). Instead, it returns an object of type std::vector<bool>::reference (a class nested inside std::vector<bool>).

There is no advantage, it is more error prevention, when you using auto with external library.

I think, this is the main idea of such idiom. You should be explicit and force auto to behave correctly.

BTW, here the nice article on GotW about auto.

Fibriform answered 1/9, 2014 at 15:15 Comment(1)
Well, std::vector<T>::operator[] always returns a std::vector<T>::reference; it just so happens that std::vector<bool>::reference is not bool& :) (replace with const_reference and bool const& for std::vector<T>::operator[] const).Repairer
C
3

Can anyone tell me, why it is advantageous to use this idiom?

The reason I can think of: because it is explicit. Consider how you would (instinctively) read this code (i.e., whithout knowing what features does):

bool priority = features(w)[5];

"Features returns an indexable sequence of some generic "boolean" values; we read the fifth one into priority".

auto priority = static_cast<bool>(features(w)[5]);

"Features returns an indexable sequence of values explicitly convertible to bool; we read the fifth one into priority".

This code is not written to optimize for shortest flexible code, but for explicitness of the result (and apparently consistency - since I assume it would not be the only variable declared with auto).

The use of auto in the declaration of priority is for keeping the code flexible to whatever expression is on the right-hand-side.

That said, I would prefer the version without an explicit cast.

Compress answered 1/9, 2014 at 14:54 Comment(4)
Except that (for parallelism with the second statement) the first is "values compatible with bool", not necessarily already bool. Or was that the point you were making using "boolean" vs bool?Permanence
"The use of auto in the declaration of priority is for keeping the code flexible to whatever expression is on the right-hand-side" -- but can auto produce any type other than bool when initialised by an expression static_cast<bool>(...)? I want to say "Of course not", in which case there's no advantage to choosing auto, but C++ has some strange corners.Gangrel
@j_random_hacker, the truth is, I'm not entirely convinced myself (i.e. I'd probably declare the variable as bool and be done with it). One advantage of keeping the static_cast (I just thought of this): When refactoring, seeing the explicit cast may be an indication that the call to features should be extracted into a function returning bool (especially if the construct repeats in client code).Compress
@BenVoigt, yes, this is what I meant (I have edited the response to make it a bit clearer). My interpretations of the client code are pretty subjective though.Compress

© 2022 - 2024 — McMap. All rights reserved.