23

In C++03, Boost's Foreach, using this interesting technique, can detect at run-time whether an expression is an lvalue or an rvalue. (I found that via this StackOverflow question: Rvalues in C++03 )

Here's a demo of this working at run-time

(This is a more basic question that arose while I was thinking about this other recent question of mine. An answer to this might help us answer that other question.)

Now that I've spelled out the question, testing rvalue-ness in C++03 at compile-time, I'll talk a little about the things I've been trying so far.

I want to be able to do this check at compile-time. It's easy in C++11, but I'm curious about C++03.

I'm trying to build upon their idea, but would be open to different approaches also. The basic idea of their technique is to put this code into a macro:

true ? rvalue_probe() : EXPRESSION;

It is 'true' on the left of the ?, and therefore we can be sure that EXPRESSION will never be evaluated. But the interesting thing is that the ?: operator behaves differently depending on whether its parameters are lvalues or rvalues (click that link above for details). In particular, it will convert our rvalue_probe object in one of two ways, depending on whether EXPRESSION is an lvalue or not:

struct rvalue_probe
{
    template< class R > operator       R () { throw "rvalue"; }
    template< class L > operator       L & () const { throw "lvalue"; }
    template< class L > operator const L & () const { throw "const lvalue"; }
};

That works at runtime because the thrown text can be caught and used to analyze whether the EXPRESSION was an lvalue or an rvalue. But I want some way to identify, at compile-time, which conversion is being used.

Now, this is potentially useful because it means that, instead of asking

Is EXPRESSION an rvalue?

we can ask:

When the compiler is compiling true ? rvalue_probe() : EXPRESSION, which of the two overloaded operators, operator X or operator X&, is selected?

( Ordinarily, you could detect which method was called by changing the return types and getting the sizeof it. But we can't do that with these conversion operators, especially when they're buried inside the ?:. )

I thought I might be able to use something like

is_reference< typeof (true ? rvalue_probe() : EXPRESSION) > :: type

If the EXPRESSION is an lvalue, then the operator& is selected and I hoped that the whole expression would then be a & type. But it doesn't seem to work. ref types and non-ref types are pretty hard (impossible?) to distinguish, especially now that I'm trying to dig inside a ?: expression to see which conversion was selected.

Here's the demo code pasted here:

#include <iostream>
using namespace std;
struct X {
        X(){}
};

X x;
X & xr = x;
const X xc;

      X   foo()  { return x; }
const X   fooc() { return x; }
      X & foor()  { return x; }
const X & foorc() { return x; }

struct rvalue_probe
{
        template< class R > operator       R () { throw "rvalue"; }
        // template< class R > operator R const () { throw "const rvalue"; } // doesn't work, don't know why
        template< class L > operator       L & () const { throw "lvalue"; }
        template< class L > operator const L & () const { throw "const lvalue"; }
};

typedef int lvalue_flag[1];
typedef int rvalue_flag[2];
template <typename T> struct isref     { static const int value = 0; typedef lvalue_flag type; };
template <typename T> struct isref<T&> { static const int value = 1; typedef rvalue_flag type; };

int main() {
        try{ true ? rvalue_probe() : x;       } catch (const char * result) { cout << result << endl; } // Y lvalue
        try{ true ? rvalue_probe() : xc;      } catch (const char * result) { cout << result << endl; } // Y const lvalue
        try{ true ? rvalue_probe() : xr;      } catch (const char * result) { cout << result << endl; } // Y       lvalue
        try{ true ? rvalue_probe() : foo();   } catch (const char * result) { cout << result << endl; } // Y rvalue
        try{ true ? rvalue_probe() : fooc();  } catch (const char * result) { cout << result << endl; } // Y rvalue
        try{ true ? rvalue_probe() : foor();  } catch (const char * result) { cout << result << endl; } // Y lvalue
        try{ true ? rvalue_probe() : foorc(); } catch (const char * result) { cout << result << endl; } // Y const lvalue

}

(I had some other code here at the end, but it's just confusing things. You don't really want to see my failed attempts at an answer! The above code demonstrates how it can test lvalue-versus-rvalue at runtime.)

Community
  • 1
  • 1
Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • @hvd, I've updated the end of the question accordingly. I should have said `fooref`, not `foo`. But anyway, typeof still thinks that `true ?: fooref() : fooref()` is *not* a reference. – Aaron McDaid Jan 31 '12 at 18:55
  • @hvd, ... I checked `typeof(x)` and `typeof(xr)` also and they are giving the same type, this doesn't make sense to me. There appear to be inconsistencies in `typeof`. I get the right behaviour with `typeof(int&)`, but it doesn't do the same thing with `typeof(xr)`. – Aaron McDaid Jan 31 '12 at 18:58
  • The type and lvalue-ness of `x` and `xr` is the same, so I'm assuming that's why `typeof`'s result is the same. There is subtly different behaviour with `decltype` that might help you out, but your version of g++ doesn't support it. –  Jan 31 '12 at 19:02
  • You try to invent standard is_lvalue_reference/is_rvalue_reference? Or I miss something. – Andrew Jan 31 '12 at 19:25
  • @Andrew Now that I see your message, I think the problem is that "x is an lvalue" and "x is a reference" are two different questions, and they are confused in the question. –  Jan 31 '12 at 20:08
  • @Andrew, is_lvalue_reference is a C++11 feature. I'm wondering how to do this in C++03. C++03 has lvalues and rvalues, and there is this `?:` that is able to distinguish them. I just want to be able take that extra step and detect this at runtime. – Aaron McDaid Jan 31 '12 at 21:06
  • @hvd, I'm not confusing the issue of references with lvalue-rvalue-ness. Boost's FOREACH shows how, in C++03, there is a way to *test* whether a given expression is an *rvalue* by looking at whether another expression based on it is of *reference type*. It's a surprising technique, but it's not a confusion on my part. I can't make it work fully. And I'm deliberately testing every version of every expression I can because I want to be sure that a function that returns a `&` is treated as an lvalue - (const vs. non-const, reference vs. non-reference, return-from-function vs named local variable) – Aaron McDaid Jan 31 '12 at 21:10
  • @AaronMcDaid Now you've lost me. `x` is an lvalue, it gets treated as an rvalue, and your comment claims the behaviour for `x` is correct. Why is that correct, then? –  Jan 31 '12 at 21:22
  • You are confused. Expressions can't have reference types. They are always either of type void or of object or function types. – Johannes Schaub - litb Jan 31 '12 at 21:23
  • @JohannesSchaub-litb Yes, that's perhaps a clearer way of explaining. It means `xr` as an expression is an lvalue expression that has type `X`. `x` as an expression is *also* an lvalue expression that has type `X`. –  Jan 31 '12 at 21:34
  • OK. Let's take a step back. I'm happy to delete most of my question and leave the raw question: "How do we replicate *at compile-time*, the functionality that Boost's FOREACH can do so well *at run-time*?". The page I linked to make clear that the underlying question is about lvalue-vs-rvalue i C++03. – Aaron McDaid Jan 31 '12 at 21:45
  • @AaronMcDaid In that case, doesn't the usual trick of `char (&helper(...))[1];` / `template char (&helper(T&))[2];` / `#define is_lvalue(x) (sizeof(helper(x)) == 2)` work? –  Jan 31 '12 at 21:53
  • I've changed the end of the question. The code I had at the end wasn't relevant. I understand the comments, but it doesn't help me to any closer to ultimate question. @JohannesSchaub-litb, an 'expression' (is that the right word?) has a 'value-category' (again, is this the right word?). If you follow the link to the Boost page you'll see clearly how the lvalue-or-rvalue-ness of an expression is related to whether `operator X` or `operator X&` is called as a converter. This is the thing I'm trying to leverage *at compile time*. – Aaron McDaid Jan 31 '12 at 21:56
  • @hvd, I've just realised that the 'reference-ness' of the type is slightly missing the point. The question I should have asked near the end of my question is "*How do we detect, at compile-time, which overloaded conversion operator (`operator X` or `operator X&`) was selected?*". I'll update my question. – Aaron McDaid Jan 31 '12 at 21:58
  • @Aaron right, but whether one of those is called has nothing to do with the type of the expression. – Johannes Schaub - litb Jan 31 '12 at 22:05
  • @AaronMcDaid Which one gets called depends exclusively on the lvalue-ness of the unevaluated expression, so if you already have a test (see my earlier comment) that tests the lvalue-ness in another way, what do you need beyond that? You could write `is_lvalue(true ? rvalue_probe() : x)`, which would give you the same result as `is_lvalue(x)`. I may well be missing something, but it would be helpful if you could clarify just what that something is. –  Jan 31 '12 at 22:09
  • @hvd, when I look at those kinds of solutions, I often find that they break on `const` lvalues. A `const` lvalue seems to appear very like an rvalue. I'll play around with that `helper` you mentioned. – Aaron McDaid Jan 31 '12 at 22:19
  • @hvd, that helper is working. It gets the wrong answer in just one case, it thinks that `const X fooc();` is an lvalue. But it's pretty weird to have const in a return value like this, so it's not a problem. But if it is that simple, then why did Boost's FOREACH go to so much trouble? ***:-)*** Maybe I'm missing something more obvious :-) – Aaron McDaid Jan 31 '12 at 22:26
  • @AaronMcDaid Yuck, you're right. A `const X` rvalue is a valid argument for a `const X &` function parameter, and that is the overload that gets used. Hold on... –  Jan 31 '12 at 22:29
  • @Xeo I'm seeing `T&` with an rvalue argument of type int deduced to `T = int`, which makes it an error. I suspect you know better than me whether that's correct. –  Jan 31 '12 at 22:56
  • @hvd: Nevermind, I somehow made the wrong assumption that `T&` fed with an rvalue of type `X` will deduce to `X const&`. This is not the case, as such my previous comments don't apply. :) – Xeo Jan 31 '12 at 23:02

2 Answers2

8

It took some effort, but here's a tested and working is_lvalue macro that correctly handles const struct S function return types. It relies on const struct S rvalues not binding to const volatile struct S&, while const struct S lvalues do.

#include <cassert>

template <typename T>
struct nondeducible
{
  typedef T type;
};

char (& is_lvalue_helper(...))[1];

template <typename T>
char (& is_lvalue_helper(T&, typename nondeducible<const volatile T&>::type))[2];

#define is_lvalue(x) (sizeof(is_lvalue_helper((x),(x))) == 2)

struct S
{
  int i;
};

template <typename T>
void test_()
{
  T a = {0};
  T& b = a;
  T (* c)() = 0;
  T& (* d)() = 0;
  assert (is_lvalue(a));
  assert (is_lvalue(b));
  assert (!is_lvalue(c()));
  assert (is_lvalue(d()));
}

template <typename T>
void test()
{
  test_<T>();
  test_<const T>();
  test_<volatile T>();
  test_<const volatile T>();
}

int main()
{
  test<int>();
  test<S>();
}

Edit: unnecessary extra parameter removed, thanks Xeo.

Edit again: As per the comments, this works with GCC but relies on unspecified behaviour in C++03 (it's valid C++11) and fails some other compilers. Extra parameter restored, which makes it work in more cases. const class rvalues give a hard error on some compilers, and give the correct result (false) on others.

  • Nevermind my last comment, Clang spews way to many errors with that for some reason... Clang also errors on your previous version, with the complete error shown [here](http://ideone.com/bCPPm). That might be a Clang bug, though. – Xeo Jan 31 '12 at 23:50
  • Ok, all of comeau, gcc and clang accept also function lvalues with Xeo's version. That clang and comeau rejects that code has to do with a clarification of C++11. They clarified that only const *non-volatile* references can bind to rvalues. C++03 just said that "binding a reference to non-const to an rvalue" is not possible. C++11 added the non-volatile part. That they still reject it in C++03 is because the reference binding itself is still ill-formed, even if overload resolution itself accepts the conversion. – Johannes Schaub - litb Jan 31 '12 at 23:55
  • Why does this work? Is it because rvalues shouldn't have any other references to them and hence are less 'volatile' than lvalues? I never really paid attention to `volatile` before - thanks for motivating me to read up on it! – Aaron McDaid Jan 31 '12 at 23:56
  • @JohannesSchaub-litb Is that a clarification or a change in behaviour? If this code works with GCC in C++03 mode because it implements the C++11 semantics, that's not something I'm happy with :) –  Feb 01 '12 at 00:01
  • @Johannes: *Can* doesn't imply *must*, so I think it's well formed, unless some other paragraph says it isn't. Also, on the "cv ref in C++03" issue: Why is the binding still ill-formed? – Xeo Feb 01 '12 at 00:04
  • @AaronMcDaid It's because function with a parameter with type `const volatile S&` cannot be called with an `const S` rvalue argument. There's a special exception that *does* allow it with `const S&`, but that exception doesn't apply to `const volatile S&`. (At least, that's the case with GCC. Whether it's true per C++03 is still an open question.) –  Feb 01 '12 at 00:06
  • @hvd I think I would take it more as a clarification but I'm so sad that clang and comeau online reject it! And please disregard my comments about function lvalues. I think it works fine with them. – Johannes Schaub - litb Feb 01 '12 at 00:06
  • @Xeo yes I think it compares "T const volatile" against "void()" (for a "void()" function lvalue) to find a match. Now "T const volatile" is const and volatile qualified, but "void()" is none of them, so they mismatch. I guess now the question is whether to transform "T const volatile" to "T" and compare again and succeed, or to try to transform "void()" to "const volatile identity::type", and be at square one, getting "void()" again and again mismatching. Apparently existing compilers choose the former route and succeed deducing T. – Johannes Schaub - litb Feb 01 '12 at 00:08
  • 3.9.3 defines "cv-qualified" only for object-types and "void" (which can serve as a source to disambiguate the above choice-ambiguity). So an impl rejecting function lvalues would seem entirely sensible to me, after all the "deduced A" is still not more cv-qualified than the original transformed A. – Johannes Schaub - litb Feb 01 '12 at 00:13
  • @JohannesSchaub-litb Thanks, but I'm confused: [over.match.viable] says "If the parameter has reference type, the implicit conversion sequence includes the operation of binding the reference", so if the binding is not valid, shouldn't the overload be removed? Or am I reading a later draft? –  Feb 01 '12 at 00:26
  • @Johannes: Back to [the clang error message](http://ideone.com/bCPPm) from the previous version: You said that the reference binding is still ill-formed for some reason in C++03. Why? – Xeo Feb 01 '12 at 00:30
  • @hvd that the implicit conversion seqence includes the operation of binding the reference means that "X&" cannot bind to an rvalue. And that "X&" is preferred over "X const&" if both work (this is *not* a qualification conversion, but a special case!). It doesn't refer to the actual initialization of the parameter that happens after overload resolution selected the function. It's the overload-resolution reference binding, so to speak. – Johannes Schaub - litb Feb 01 '12 at 00:32
  • @Johannes: Ah, so it's a bug if read in one way, and conforming if read another way in C++03? – Xeo Feb 01 '12 at 00:37
  • @JohannesSchaub-litb [over.ics.ref] seems not to answer it either way: it first says that binding a non-const reference to a temporary is a no-go, it then says that other restrictions not based on the type of the reference and the argument are not checked. But this is another restriction that *is* based on the type of the reference and the argument. –  Feb 01 '12 at 00:38
  • @Johannes: I'm thinking that this might call for a defect report to the C++ standard. It either needs to be clarified, or the overload resolution ICS rules need to be adjusted to match the normal ref binding rules. Opinions? – Xeo Feb 01 '12 at 02:25
  • @Xeo C++11 is clear on the issue: http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1152 –  Feb 01 '12 at 02:38
  • @hvd: Ah, okay. I think I got it now... Binding an rvalue to ref-to-cv is completely ill-formed in C++11 and should exclude an overload from the set of matches (Clang buggy here). In C++03, it's fuzzy and not clear. Correct? – Xeo Feb 01 '12 at 02:58
  • @Xeo Yes, pretty much, you're not allowed to bind an rvalue to a ref-to-cv in either C++11 or C++03, but C++11 actually says this causes overloads to be excluded, and C++03 is silent on that last part. –  Feb 01 '12 at 03:13
  • Hey guys, I read those comments but it's mostly too advanced for me :-) Thanks anyway, I think I picked up something. Anyway, what's the bottom line? That there might be a c++03 compiler which binds `const volatile &` to an rvalue, *and* that they could almost argue they are a conforming compiler? And C++11 is clearer on the topic - that this behaviour is just not allowed? – Aaron McDaid Feb 01 '12 at 11:50
  • @hvd no. the existence of a conversion sequence is independent of whether the initialization of a variable is well formed. Overload resolution is eager, it accepts conversions of arguments to parameters even if the initialization of the parameter will later be ill-formed. For example, access violations (private base class), and this very reference initialization issue. The inconsistency you mention was fixed in C++11, but nontheless an inconsistency is not necessarily a contradiction. – Johannes Schaub - litb Feb 01 '12 at 11:53
  • @JohannesSchaub-litb I didn't claim otherwise; the standard specifically addresses some cases where overload resolution doesn't discard a function even though it won't be callable. What I'm saying is that the description of reference binding is clear in what is and isn't included, except for this one corner case, which is included by some compilers (matching C++11) and excluded by others. –  Feb 01 '12 at 12:04
1

The address-of operator (&) can only be used with an lvalue. So if you used it in an SFINAE test, you could distinguish at compile-time.

A static assertion could look like:

#define STATIC_ASSERT_IS_LVALUE(x) ( (sizeof &(x)), (x) )

A trait version might be:

template<typename T>
struct has_lvalue_subscript
{
    typedef char yes[1];
    typedef char no[2];

    yes fn( char (*)[sizeof (&(((T*)0)->operator[](0))] );
    no fn(...);
    enum { value = sizeof(fn(0)) == 1 };
};

and could be used like

has_lvalue_subscript< std::vector<int> >::value

(Warning: not tested)

I can't think of any way to test an arbitrary expression valid in the caller's context, without breaking compilation on failure.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Can the address-of operator not be used with rvalues of class type that have their own `operator&` function? –  Jan 31 '12 at 20:01
  • I like this. It's not a full solution yet, but it is correct. I'm experimenting a little with this now. – Aaron McDaid Jan 31 '12 at 22:08
  • @hvd: It's hard to say what result you want for a class weird enough to overload `operator&`. It probably is trying to transparently wrap an lvalue, in which case maybe this test should say it IS an lvalue. – Ben Voigt Jan 31 '12 at 22:41
  • I think I envisage having a macro called `STATIC_ASSERT_IS_LVALUE(x)` which will cause a compilation failure if x isn't an lvalue (and which will also evaluate to x). Taking the `&` would be good enough for me. If we can just write up the assertion, then I think I'll accept this. What about using the comma operator? `#define STATIC_ASSERT_IS_LVALUE(x) ( (typeof(&x) ) 0 , x)` – Aaron McDaid Jan 31 '12 at 22:59
  • ... ultimately, it would be nice if it did something other than force a compilation error. For example, somebody might want a `STATIC_ASSERT_IS_RVALUE` instead. But the former would be good enough for me. – Aaron McDaid Jan 31 '12 at 23:04
  • @AaronMcDaid That one's easier: `reinterpret_cast((x))` is a hard error if `x` is not an lvalue -- that one does work with `const T` function return types. –  Jan 31 '12 at 23:04
  • @AaronMcDaid You can of course wrap that in `sizeof` to get an unevaluated expression. –  Jan 31 '12 at 23:05
  • @AaronMcDaid And by "does work" I mean "does generate the intended error" –  Jan 31 '12 at 23:06
  • Definitely prefer `sizeof` over `typeof`, since `sizeof` is standard. – Ben Voigt Jan 31 '12 at 23:18