10 comments on “Tippet: Use reference_wrapper to create views of data

  1. I think references are safer than pointers, but only to an extent. They are not, in particular, what some articles encourage you to believe—”safe”. Yes, dereferencing null is undefined behaviour, but so does dereferencing an invalid address. The difference between the two is that it’s easy to check for null, and even if you forget to do that, it’s often easier to trace down a null dereference than an invalid memory dereference. So references help in mitigating the easier case of nulls, but not in the slightest with the nastier case of invalid memory.

    Just as “use a reference” is an “easy” solution against forgetting to assign a value, assigning a value is an “easy” solution against forgetting to use a reference. Or, so is sprinkling const everywhere and bumping up the warning level.

    I think a good argument for references is “references can’t be null, must be assigned to, and can’t be reassigned to, so they reduce fragility and therefore are great if you’re cool with these constraints”. But equally, pointer const* const are great for similar reasons, require cooperation in a function call func(&v) as opposed to func(v), and go especially well with design by contract, so they too are great if you accept the constraints.

    In fact, one might argue “optional value” is something to avoid, because that too leads to complexity in code: you have to check for it. For example, the fact that an if conditional doesn’t require an else is a point against using if, and maybe in favour of using the ternary operator ?:. But sometimes an if is exactly what we want, expresses better what we mean, and we can use that safely. Often not not, but sometimes yes.

    I think the lesson to learn is that it’s important to study the characteristics of a feature or a tool, and choose a tool for expressing ourselves wisely. Not to suggest that one is safe and the other isn’t. It’s rarely black and white like that.

    • Vague platitudes like that are pretty useless in practice, and actually detrimental to beginners. Experienced C++ers will already know a dozen techniques that would work and how to choose between them; they don’t need you nor I to tell them to do that, so that advice is just useless for them. Newbies, however, do not have the knowledge or experience to properly judge the appropriateness of the few techniques they do know, and shouldn’t even try to decide on their own – with their limited knowledge and understanding – what is “best”; that kind of advice – to “choose wisely” when they don’t yet have the wisdom to do – is actually bad advice for them.

      Saying “this technique is good, that technique is evil” helps beginners (assuming the one technique is actually superior to the other in the majority of cases) because the beginners will easily remember simple guidelines like that. They will also apply them “dumbly” until such time that they have the knowledge and experience to properly assess on their own which technique is best… but that’s a good thing, because they will be using “good” (generally applicable, safe) techniques, rather than “evil” (risky, limited-use) ones. As for experts, they’ll just tune out the guideline and do what they know is right anyway.

      • You are not going to get far if you treat people like idiots. There is little that differentiates between you and a “beginner”, to use your language, apart from just a bit of time. You are not smarter than a “beginner”. In fact, countless “beginners” are smarter than you and me combined.

        You also are not going to get a lot of respect if you patronize people.

        • While I appreciate your theories on teaching that you probably thought up last night, and your advice on how I might “get far”, I’ve been teaching C++ for almost 15 years now. Quite successfully, too. During that time, I’ve put quite a bit of thought into how to do it best.

          You can’t simply give the same set of tools to a pro and a beginner and expect the beginner to do a pro’s job, even if you give them a lot more time to do it in. That’s just ridiculous.

          A beginner to C++ faces an ocean of knowledge and experience they don’t have. To help them dive in, you have to hand them a set of tools that are a) easy to understand (without requiring the user already have the whole body of knowledge in the field); b) useful in as many places as possible; and c) hard to screw up with. With tools like that, not only will they write better code right out of the gate, it’s easier for them to experiment, which will inevitably lead to picking up more knowledge and experience, and more tools for their toolbox. As they learn more, they will start to decide for themselves when to start ignoring the more simplistic advice use more advanced (and more dangerous) tools. That’s pretty much the opposite of “patronizing”; that’s giving people good directions to start, then letting them find their own way from there.

          You can’t just give a beginner the same advice you’d give an expert then dust your hands and think you’ve done a good job helping them because they’ll “figure it out in time”. That’s not because beginners are “idiots”, it’s because they’re beginners – they don’t yet have the knowledge and experience to make the same kinds of decisions that an expert does (which is why they’re called “beginners”, to use “my language” – “beginner” is not an insult, it’s a fact of life; a phase we’ve all passed through). That’s why you give them simpler and safer tools to let them build experience, so they can eventually make more informed decisions on their own.

  2. First, your final example is not really C++11, it requires post-C++11 features. I mention it because your post it tagged with ‘C++11’.

    Some lines look non-idiomatic.

    For example, why write

    auto const jobs = vector<job>{};

    when

    const vector<job> jobs;

    has the same effect and is shorter.

    Or just write

    // dummy
    vector<job> get_jobs() { return vector<job>{}; }
    // ...

    auto const jobs = get_jobs();

    to indicate that your empty const vector is just a placeholder.

    Same goes for the views, e.g.

    auto time_view = vector<reference_wrapper<job const>>{};

    vs.

    vector<reference_wrapper<job const>> time_view;

    Again, less to type, less noise. It is a kind of auto-overuse.

    The following initialization is non-idiomatic

    transform(begin(jobs), end(jobs),
    back_inserter(time_view), make_job_ref);

    because the view can be initialized during construction like this:

    vector<reference_wrapper<job const>> time_view(begin(time_view), end(time_view));

    This also eliminates the need for defining the function make_job_ref.

    Using a static member function as a comparator for sorting is unusual –
    the idiomatic way would be to use a function object like this:

    struct job
    {
    // ...
    struct order_by_time {
    bool operator()(job const& a, job const& b) const
    {
    return a.time < b.time;
    }
    };
    };

    sort(begin(time_view), end(time_view),
    job::order_by_time());

    This is also guaranteed to get inlined.

    Since the article is about C++11 – one wonders, why range-for loops are ignored and instead old-school std::for_each algorithms are used , e.g.:

    for_each(begin(jobs), end(jobs), print_job);

    vs.

    for (auto &j : jobs) print_job(j);

    Like before, less to type, clearer.

    Or instead of

    for_each(begin(time_view), end(time_view),
    print_job);

    this

    for (auto &j : time_view)
    print_job(j);

    • First, your final example is not really C++11, it requires post-C++11 features. I mention it because your post it tagged with ‘C++11′.

      All of the code on my blog is modern C++ – this is a modern C++ blog. Unless otherwise indicated, all code is written in the most current version of C++ – which at the time of writing is C++14. The post is tagged “C++11” because the technique that the post is about requires (at least) C++11, not because I’m going to hamstring myself and write code to an old standard.

      Besides, none of the code presented in the post was the point of the post – it was all just illustration of the technique. It’s not supposed to be copy-pasted, so who cares what version of the standard it’s written in? I could even write it in pseudocode. Because why not?

      For example, why write…

      Why are you concerned not only with bits of code used as illustration, but with bits of code that are entirely irrelevant?

      If you must know, the code was originally written auto const jobs = get_jobs(); followed by a comment about get_jobs() returning some kind of container of jobs – specifically to make the point that it could be any container. I decided that might be a little too obtuse for some people, so I changed it to show an actual type (a vector) with a comment about filling it up separately.

      As for why it’s written using the auto style, that’s because I don’t believe in using three or four different styles of declaration – I write all my code with a single style (except where the language itself won’t allow it). Of all the declaration styles, the auto style is the best. It is the most consistent, the safest, the easiest for beginners to read and understand, and the hardest for them to screw up.

      Again, less to type, less noise. It is a kind of auto-overuse.

      “Less to type” is not really a great virtue. Especially when the “less to type” style creates the possibility of errors and confusion for beginners. It happens to work out fine in this case, but if time_view were an int, you’d have an uninitialized value. (And if you want to try to “fix” it by insisting on braces, you’re opening up a whole new can of worms when dealing with initializer lists, optional equal signs, and quirky differences between C++11 and C++14. That’s a fast track to confusing the hell out of beginners.)

      The auto style just works. It’s easy to understand, it’s the same in every context – whether creating a new object or getting one from a function – it’s easy for beginners to spot declarations (just trail your eyes down the left and look for autos; they’ll either be declaring objects or functions, which you can tell by whether there’s a parentheses or an equal sign), and it’s functionally impossible to screw up (you can’t possibly make an uninitialized variable or trigger a surprising conversion). The only case where it doesn’t work is the extremely rare case of non-copyable, non-moveable types (and those types are usually special enough that they warrant unique attention).

      The following initialization is non-idiomatic…

      You use the word “idiomatic” to mean “stuff you’re familiar with” or “stuff you see often”. The actual word means something entirely different.

      The reason the two views are constructed that way is two-fold. First, to change as little code as possible between the “original” and “fixed” examples. Second, as a set up for a future extension to this post where I show how I’ve just covered the tip of the iceberg here. If you want a sneak preview, the future post (if I ever do it) would show that you don’t necessarily need to take all the elements of the source set (you can filter some out), you can make views with elements from multiple sources, and you can even make views of parts of elements. (I had intended to put all that in one post, but it got unwieldy.)

      Since the article is about C++11 – one wonders, why range-for loops are ignored and instead old-school std::for_each algorithms are used

      Because one should never use a plain for loop when there is an algorithm that does the same job, ever. That includes range-for. Algorithms are superior in every way to manually written loops… with only one exception: the standard does not yet have range algorithms (so you have to manually write the begin and end), so writing a range-for (in plain standard C++, without Boost) is slightly easier, for now. But it’s still not “easy” – for example, for all your talk of “idiomatic” C++, the range-for loops you have written are not proper form.

      The point of this post was not to demonstrate how to write for-each operations. All of that code was entirely incidental to the topic, and used only for illustration. And it was deliberately written only using pure standard C++, even though that’s not the best option. A better option would be to use Boost’s range algorithms, or write your own, or you can wait for range algorithms to be standardized, but either way, you should never write a for loop when there is an algorithm that does the job.

      For the record, I limited the illustration code – which, again, let me stress, was just code intended to illustrate the concept, not actual code you are meant to copy-paste – to stuff currently available in the standard, but if I were to recommend how to write the print lines properly, it would be more like cout < < "Jobs:\n\t" << write_all(jobs, "\n\t") << '\n';, or for the indirected views: cout < < "View:\n\t" << write_all(pointer_view | indirected, "\n\t") << '\n';.

  3. Interesting technique.

    What would also be interesting is creating an own wrapper around shared_ptr which forwards arguments on construction and also has the conversion operator definded.

    Basically having an explicitly shared object acting as if it was a reference.

    • Bjarne Stroustrup is pushing hard for an operator. (operator dot), that would presumably allow for a “shared reference” along the lines of what you suggest. (It would also make reference_wrapper a perfect proxy, or at least much closer to it – you won’t need get() or trickery to cast to T& anymore.)

      I’m a little fuzzy on exactly how the dot operator will work, and how it will interact with the proposed uniform function call thingy, (mostly because there are multiple, conflicting proposals for each) but it seems to me that if we wait a year or two, what you’re looking for will probably be in the standard – in totally generic form, too.

Leave a Reply

Your email address will not be published. Required fields are marked *