C++03. 在编译时测试rvalue-vs-lvalue,而不仅仅是在运行时进行测试。

23
在C++03中,使用Boost的Foreach,使用这个有趣的技术,可以在运行时检测一个表达式是左值还是右值。(我通过这个StackOverflow问题发现的:C++03中的Rvalues)

这里是一个演示在运行时工作的例子

(这是一个更基本的问题,当我思考我的另一个最近的问题时出现了。这个问题的答案可能会帮助我们回答那个其他的问题。)

既然我已经阐述了问题,在C++03中测试rvalue-ness是否为编译时,我将谈论一下我迄今为止尝试过的事情。

我想能够在编译时进行此检查。在C++11中很容易,但我对C++03很好奇。

我正在尝试借鉴他们的想法,但也愿意接受不同的方法。他们的技术的基本思路是将这段代码放入宏中:

true ? rvalue_probe() : EXPRESSION;

?的左侧是"true",因此我们可以确定EXPRESSION永远不会被评估。但有趣的是,?:运算符的行为取决于其参数是lvalue还是rvalue(点击上面的链接获取详细信息)。特别地,根据EXPRESSION是否是lvalue,它将以两种方式之一转换我们的rvalue_probe对象:

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"; }
};

那么在运行时这是可行的,因为抛出的文本可以被捕获并用于分析表达式是左值还是右值。但我希望找到一种方法来在编译时识别正在使用哪种转换方式。

现在,这有潜在的用途,因为它意味着我们可以问:

当编译器编译true ? rvalue_probe() : EXPRESSION时,两个重载运算符中的哪一个operator Xoperator X&被选中?

(通常情况下,您可以通过更改返回类型并获取其sizeof来检测调用了哪个方法。但是我们无法使用这些转换运算符,特别是当它们被嵌套在?:中时。)

我想我可以使用类似以下内容的东西:

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

如果表达式是lvalue,则选择operator&,我希望整个表达式都是&类型。但似乎并不起作用。引用类型和非引用类型很难(或者说不可能)区分,尤其是现在我正在尝试挖掘?:表达式以查看选择了哪种转换。以下是演示代码:
#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

}

我在这里有一些其他的代码,但它只会让事情变得混乱。你并不想看到我对答案的失败尝试!上面的代码演示了如何在运行时测试左值与右值。


@hvd,我没有将引用的问题与lvalue-rvalue-ness混淆。Boost的FOREACH展示了在C++03中测试一个给定表达式是否为rvalue的一种方法,方法是查看基于其的另一个表达式是否为引用类型。这是一种令人惊讶的技术,但这不是我的混淆。我无法完全使其工作。我有意测试可以找到的每个表达式版本的每个版本,因为我希望确保返回&的函数被视为lvalue - (const vs. non-const, reference vs. non-reference, return-from-function vs named local variable)。 - Aaron McDaid
@AaronMcDaid 现在你把我搞糊涂了。x 是一个左值,但它被视为右值,而你的评论声称 x 的行为是正确的。那么为什么是正确的呢? - user743382
好的。让我们退一步。我很乐意删除我的大部分问题,只留下原始问题:“我们如何在编译时复制 Boost 的 FOREACH 在运行时所能做得很好的功能?” 我链接的页面清楚地表明,根本问题是关于 C++03 中的左值与右值。 - Aaron McDaid
在这种情况下,char (&helper(...))[1]; / template <typename T> char (&helper(T&))[2]; / #define is_lvalue(x) (sizeof(helper(x)) == 2) 这种常规技巧不起作用吗? - user743382
@hvd,当我看到那些解决方案时,我经常发现它们会在const lvalue上出问题。一个const lvalue似乎非常像一个rvalue。我会试着玩一下你提到的那个“helper”。 - Aaron McDaid
显示剩余16条评论
2个回答

8

花费了一些精力,但这里有一个经过测试并且可行的is_lvalue宏,在正确处理const struct S函数返回类型时。它依赖于const struct S右值不会绑定到const volatile struct S&,而const struct S左值会。

#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>();
}

编辑: 多余的参数已被删除,感谢 Xeo。

再次编辑: 根据评论所说,这个方法在GCC中可以工作,但它依赖于C++03中未指定的行为(在C++11中是有效的),并且在其他一些编译器中无法工作。我们恢复了额外的参数,这使得它在更多的情况下可以工作。在某些编译器上,const类rvalue会产生严格的错误,并在其他编译器上给出正确的结果(false)。


为什么这个能够工作?是因为rvalue不应该有任何其他引用,因此比lvalue更加“稳定”吗?我以前从来没有注意过volatile - 谢谢你激励我去研究它! - Aaron McDaid
@hvd 我认为我会将其视为澄清,但我很难过clang和comeau在线拒绝它!请忽略我的有关函数lvalues的评论。我认为它们可以正常工作。 - Johannes Schaub - litb
@Xeo 是的,我认为它将“T const volatile”与“void()”(对于“void()”函数lvalue)进行比较以找到匹配项。现在,“T const volatile”是const和volatile限定的,但“void()”没有这些限定,所以它们不匹配。我猜现在的问题是是否将“T const volatile”转换为“T”并再次比较并成功,还是尝试将“void()”转换为“const volatile identity<void()>::type”,并回到原点,一遍又一遍地得到“void()”不匹配。显然,现有的编译器选择前者路线并成功推导出T。 - Johannes Schaub - litb
@JohannesSchaub-litb 谢谢,但我有点困惑:[over.match.viable]说“如果参数具有引用类型,则隐式转换序列包括绑定引用的操作”,所以如果绑定无效,重载是否应该被删除?还是我在阅读以后的草案? - user743382
@Johannes: 啊,所以在C++03中,如果以一种方式读取,则存在错误,而以另一种方式读取则符合要求? - Xeo
显示剩余16条评论

1
地址运算符(&)只能与左值一起使用。因此,如果您在SFINAE测试中使用它,您可以在编译时进行区分。
静态断言可能如下所示:
#define STATIC_ASSERT_IS_LVALUE(x) ( (sizeof &(x)), (x) )

一个特质版本可能是:

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 };
};

并且可以像这样使用

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

(警告:未经测试)

我无法想到任何一种方法来测试调用者上下文中的任意表达式是否有效,而不会在失败时破坏编译。


具有自己的operator&函数的类类型的rvalue是否不能使用取地址运算符? - user743382
我喜欢这个。虽然它还不是完整的解决方案,但是它是正确的。我现在正在进行一些实验。 - Aaron McDaid
@hvd:很难说你想要一个重载operator&的奇怪类的结果是什么。它可能正在尝试透明地包装一个左值,在这种情况下,也许这个测试应该说它是一个左值。 - Ben Voigt
@AaronMcDaid 当然,你可以将其包装在 sizeof 中以获得未评估的表达式。 - user743382
@AaronMcDaid 而“does work”的意思是“确实生成了预期的错误”。 - user743382
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接