28 comments

[ 3.2 ms ] story [ 56.5 ms ] thread
Not a bad article, but I'm so sick of the term "universal references". Perhaps useful for explaining things to the uninitiate, but a more technical understanding requires the knowledge that rvalue references can be made from any l- or rvalue.

So when I see "Don't let T&& fool you here - T is not an rvalue reference" -- yes it fricking is! And if you're going to work on a low enough type-level that you need perfect forwarding, you NEED TO KNOW THAT.

| but a more technical understanding requires the knowledge that rvalue references can bind to any l- or rvalue

I don't think that's true. rvalue references can only bind to rvalues.

Here's a complete program. foo(i) won't compile bacause i is not an rvalue. bar(i) will compile, and foo(1) will compile.

#include <iostream>

using namespace std;

int foo(int&& i){ return i; }

template<class T> T bar(T&& i){ return i; }

int main() { int i=0; cout << foo(i) << endl;

   return 0;
}

edit: I am replying humbly. C++ is a complex language and I might very well be wrong.

I did not mean "any rvalue-reference" and bind to "any l- or rvalue", but that you can have an "int &&", "int& &&", "const int& &&", and even "int&& &&". I updated my comment to say "can be made from" instead "can bind to".

Your code example does not work because you call "foo" instead of "bar". But consider this one:

#include <iostream>

using namespace std;

template<class T> T bar(T&& i){ return std::forward<T>(i); }

int main() {

  int i=0;

  cout << bar<int&&>(6) << endl;

  return 0;

}
Maybe you're confusing reference collapsing with universal references?

In your example, replace bar<int&&>(6) to bar<int&&>(i).

It won't compile because i is not an r value. bar(i) will compile, because the template parameter is a universal reverence that will become an lvalue (not an rvalue) through reference collapsing.

I called "bar" with "<int&&>" to demonstrate that you can bind an rvalue-ref to an rvalue-ref, not to suggest that's how it should be called.
At C++Now 2014 there was some agreement on renaming 'universal references' to 'forwarding references'. Apparently, Scott Meyers, the guy that came up with the term universal references, even agreed to update his upcoming book with the new term.
At least "forwarding" specifies "for use in perfect forwarding", but it must be made clear that this term refers to the syntactic phenomena and they are not distinct from rvalue references.
> So when I see "Don't let T&& fool you here - T is not an rvalue reference" -- yes it fricking is!

T&& is only an rvalue reference when T is a non-lvalue-reference type. If T = U& then T&& = U& because of the reference collapsing rules. If you're saying that you should understand how type deduction works and interacts with reference collapsing if you're going to write C++11 then I agree. Like much of C++, perfect forwarding's design is an ugly mess and is not amenable to a superficial understanding unless you like shooting yourself in the foot.

This is exactly what the linked article says about universal references, by the way. Did you actually read it, or just got angry at the title?
I find this stuff frustrating because it's so hard to figure out if I'm implementing it properly. I can't rely on trial and error, or compiler verification, because errors manifest as extra copying or other inefficiencies, instead of observably wrong behavior.

In fact it's often hard to even figure out what's going on at all. For example, in the MyKlass case.

    v.push_back(MyKlass(2, 3.14f));
What's being invoked: a move constructor or copy constructor? How would you verify? You could maybe add a move or copy constructor with some logging, but defining either of those can change which one gets invoked! So I guess you have to hunt through the assembly.

Here's another case that terrifies me. Testing with clang 3.5:

    return a; // invokes a's move constructor
    return true ? a : a; // invokes a's copy constructor
    if (true) return a; else return a; // elides constructors using RVO
So even when you do get it right, seemingly irrelevant changes can silently break it. It's so fragile!

How do other C++ programmers verify that they're getting this stuff right?

How do other C++ programmers verify that they're getting this stuff right?

I shared your frustation then recognized it's origin: me not properly understanding the stuff because of lack of knowledge and training. So I spent a couple of hours searching the internet, writing some code and verifying behavior (1) until I knew what I needed to know about the subject. Then applied that knowlegde to some heavy template code using all new features (variadic templates/perfect forwarding/...). Figured I still didn't know enough so I learnt some more. In the end it took a while, but now I'm pretty confident with it and the resulting code benefits from it. I used to care way too much about spurious copies etc but lately I found that I hardly make classes copyable anymore, only movable, which also automatically seemed to make typical issues with sharing and lifetime fade away.

(1) sometimes looking at assembly, which is always a good way to get deep understaning, mostly by using 'Noisy' classes like

    class Noisy
    {
    public:
      Noisy() { print( "ctor" ); }
      Noisy( Noisy&& ) { print( "move ctor" ); }
      //etc, you get the point
    };
and using it in cases like yours. And yes defining move vs copy constructor might change what gets invoked but if you have to care that much which one you should ask yourself if you need the copying.
Shouldn't your move vs copy constructors be generally irrelevant? The idea is that they are semantically equivalent thus the compiler can make the correct choice for you.

Your comment about RVO isn't accurate, those are two separate returns with rvalues (which can thus use a move constructor).

I would heartily agree that tooling to allow inspection about how all of these rules apply would help tremendously.

These days I am writing a lot of C++11 code. I find that I write two different kinds of objects:

1. copyable objects like

    class RGB { int r, g, b };
2. big complex things that should never copy but can move

    class Foo { Foo(const Foo&) = delete; };
In case of (1), I don't care if move or copy constructors are used and I am happy that the compiler will decide. In case of (2), if by accident I invoke a copy constructor, I end up getting a compile-time error.
I am not sure what C++ brings in as a language.

If you want to get an idea how your code will work or even if it going to work at all you have to first understand type deduction rules which are seemingly completely unprincipled. When I compare the "rules" of C++ type deduction to functional languages' type inference algorithms I can easily understand how the latter work but I have no clue about what justifies the former.

Yet, in C++, if I want auto type inference on my recursive factorial function I have to write "return 1;" syntactically before (i.e. where the order is the relative position of bytes fed to the compiler) "return n*fact(n-1);", otherwise the compiler is unable to find the type. I just don't understand. The type deduction rules (say reference collapsing) are rocket science and take a full blog post as their justification, but basic unification algorithms known in the PL community for literally decades cannot be implemented in the standard?

This language seems plain crazy, this kind of article makes me wonder why people think it can be a reasonable choice for any kind of software project.

At least, C can fit in a brain.

I have for a long time considered C++ to be a language creation language, and to understand a particular codebase in C++, you'd have to survey the whole thing first for the greater patterns that could be redesigning the way the whole codebase works. All the debates of static vs. dynamic, strong vs. weak, GC vs not, are arguments you can play out within in the same file of a C++ project. I think this is why arguments about it take hold so much, that is, given various conditions, just about anyone's arguments about C++ are right.
I thought functional languages had to give up on most forms of overloading as well as implicit conversions for their algorithms to work.

While the former is not a big deal, the latter would be a deal breaker (as much as I have learned to live without).

You are right. But these problems can be solved using annotations.

Your comment helped me to narrow my complaint: While functional languages require annotations for things that are not obvious (when to subclass, when to change the type of an object, what overload should I use) and infer the obvious ("n*fact(n-1)" is an integer); C++ infers the non-obvious and requires you to add annotations on the obvious!

Phrased that way, it is striking.

> I am not sure what C++ brings in as a language.

C speed of execution, while offering support for large scale programming, OOP, functional, generic and plain old imperative programming.

Ability to treat user defined types as first class citizens.

Standard library that offers all mechanisms to avoid the traditional C pitfalls in terms of memory/resource leaks and out-of-bounds errors.

> This language seems plain crazy, this kind of article makes me wonder why people think it can be a reasonable choice for any kind of software project.

Except for Ada, very few languages offer similar capabilities and Pascal like syntax is out of fashion.

> At least, C can fit in a brain.

Can you master all compiler specific behaviours not covered by the standard?!?

> Can you master all compiler specific behaviours not covered by the standard?!?

No, and the good part is: I don't need to :). All my programs are standard C11 (- concurrency).

So how you fit it all in the brain, then?
I think his point was that the standard (even C11) leaves a great deal of implementation-specific behavior.

C++11 has the same problem (as it has the same implementation specific behavior as C), but the standard library allows you to avoid most of it.

> At least, C can fit in a brain.

A typical programmer's mental model of C can fit in a brain. Actual C is every bit as complex and nuanced as C++; it just has a narrower scope. I mean, C++ may be crazy, but don't pretend a language where

    uint64_t foo = 1 << 48;
has undefined behaviour,

    int a [3];
declares an array while

    void foo (int a [3]);
declares a function taking a pointer, and polymorphism is achieved by erasing all types and parsing a string at run time to figure out where to pull stuff off the stack:

    printf ("%d%s\n", 3.0, " s"); // Whoops, "%d" doesn't mean "double." It's a shame I don't have a type system for catching that sort of error.
is anything remotely resembling sane.
Pretty crazy; but C++ has extra crazy all its own, so it wins the crazy contest.
I won't contest that! But C is crazy enough that suggesting it as an alternative to C++ because C++ is crazy is, well, crazy. If you can handle the crazy of C, you can probably handle C++, because the crazy specific to C++ usually at least converges to reasonable behaviour.

I mean, you'll drive yourself nuts trying to wrap your head entirely around either language, so you may as well at least mentally approximate the one that gives you basic type-parameterization.

C++ is getting more complicated and hence less interesting every day.

How is Rust solving this particular issue?

Possibly like this: http://discuss.rust-lang.org/t/pre-rfc-placement-box-with-pl...

It looks like it'll be supported first-class for 1.0. By my guess we'll be writing things like this:

    // NOTE: This is not real code, just a guess on my part!
    let heavy_objects: Vec<Heavy> = vec!();
    // Construct a new Heavy in-place at the end of the vector.
    box(heavy_objects.emplace_back()) Heavy::new();
The fact they won't deprecate anything is appalling. There are warts on warts on warts for sure and just about any criticism of C++ has some basis.

It may be surprising for you to learn that from the perspective of a programming something C++11 is simpler and more straightforward than c++ was previously. Believe it or not. I know didn't think it would be.

They do deprecate things, e.g. exception specification and the auto keywords was re-purposed.