operator auto

In a previous post on parameter-only types, I mentioned in passing the idea of operator auto. For the past couple of years, when I’ve used that phrase, I’ve meant something like what’s proposed in Joël Falcou, Peter Gottschling, and Herb Sutter’s P0672 “Implicit Evaluation of auto Variables” (2017). That is, we have some class with a bunch of operators that return expression templates, or perhaps just proxies (like vector<bool>::iterator). Anyway, they return objects of some ugly temporary type, probably containing dangling references.

template<class Left, class Right, other, stuff>
class ExpressionTemplate {
    Left *left; Right *right;
    other stuff;
};

class Matrix {
    Matrix(ExpressionTemplate&&);

    friend ExpressionTemplate<some, stuff>
    operator+(const Matrix&, const Matrix&);

    friend ExpressionTemplate<different, stuff>
    operator+(const Matrix&, ExpressionTemplate<some, stuff>);

    friend ExpressionTemplate<different, stuff>
    operator+(ExpressionTemplate<some, stuff>, const Matrix&);
};

So now when we write

Matrix a, b;

Matrix c = a + b + Matrix(1,0, 0,1);

it temporarily generates a bunch of intermediate objects of type ExpressionTemplate<some, complicated, stuff>, each of which contains pointers to other objects in the expression. The top-level ExpressionTemplate object contains a left pointer to the temporary ExpressionTemplate object representing a + b, and a right pointer to our temporary (rvalue) Matrix object. But then that ExpressionTemplate is implicitly converted back into a Matrix, which does the math and reifies the result with no more pointers, and we’re all good.

But if we accidentally write

auto c = a + b + Matrix(1,0, 0,1);

this is suddenly not fine! Now the type of c is ExpressionTemplate, and it contains pointers to temporary objects whose lifetime has ended — they’re gone! Now when we try to use c as a Matrix, either it won’t compile (the lucky case) or it will implicitly convert to Matrix which will try to do the math (on objects which have disappeared) and it’ll segfault (the semi-lucky case) or just give the wrong answer (the worst case).

Falcou et al. want to solve this by letting the ExpressionTemplate class indicate, “When I am captured by auto, here is the type I actually want to be captured as.” In this case, we want ExpressionTemplate to be captured as Matrix.

As mentioned in my previous post, C++’s native reference types already behave as if they have an implicit operator auto. When you auto a reference type, you don’t get the actual reference type — you get the referenced type!

template<class T>
class NativeReferenceTo {
    T *ptr;

    // P0672 proposed this syntax, among others
    T operator auto() const { return *ptr; }
};

Anyway, at the ACCU Bay Area C++ meetup last night, in a conversation afterward I mentioned operator auto, and Bryce Adelstein Lelbach informed me that operator auto was already in the language! “That’s a deduction context,” he said. And he was right!

struct S {
    operator auto() { return 42; }
};

int main() {
    S s;
    double x = s;
}

This is a valid C++14 program. But it doesn’t do what you’d expect (even if you expected it to compile, which I certainly didn’t!). Here the word auto is essentially standing in for the return type of the function, and then C++14’s “return type deduction” kicks in to deduce the return type of the function as decltype(42) i.e. int. So this S is precisely equivalent to

struct S {
    operator int() { return 42; }
};

Notice that this is grossly different from

struct S2 {
    template<class T>
    operator T() { return 42; }
};

In case S2, the conversion operator’s return type is deduced from its call site, not from its body. Converting S2 to X will always work, and always yield X{42}.

And then, if we combine both…

struct S3 {
    template<class T>
    operator auto() { return 42; }
};

…GCC helpfully warns us that this member function cannot ever be called, because it needs to deduce T from its call site, and its call site needs to consider all possible operator Xs to perform overload resolution, and its call site isn’t allowed to instantiate any of the operator Xs until it’s picked the right one. In other words, this seems to be the one place in C++ where the caller performs overload resolution based on a function’s return type instead of based on its parameter types. So naturally this does not play well with C++14 return type deduction.

If we give T a default, so that it doesn’t need to be deduced, then GCC and Clang disagree: GCC says “I can’t figure out which operator X you want me to instantiate first,” whereas Clang somehow eagerly instantiates enough of the function body to find out its return type, and thereafter treats the auto as equivalent to int.

Modest proposal: let’s remove operator auto from C++, thus freeing up more real estate for P0672, and also eliminating the source of blog posts like this one.

Posted 2018-07-12