元组的一个元素可以引用另一个吗?

时间:2012-07-26 18:25:43

标签: c++ c++11 tuples undefined-behavior

更新:请参阅下面的完整答案。简短的回答是不,不是直接的。您可以使用std::reference_wrapper创建间接引用,或者通过指针更广泛地实现相同的效果(但没有语法糖和附加的引用安全性)。

我问,因为元组在C ++ 11中构成了一个方便的可变存储单元。从理论上讲,元组的一个元素保持对同一元组中另一个元素的引用听起来是合理的。 (用“指针”替换“引用”,它在实践中起作用。)魔鬼是构造这样一个元组的细节。请考虑以下示例:

#include <tuple>
#include <iostream>

class A
{
public:
  A() : val(42) { }
  int val;
};

class B
{
public:
  B(A &a) : _a(a) { }
  int val() { return _a.val; }

private:
  A &_a;
};

int main()
{
  A a;
  B b(a);
  std::tuple<A, B> t1(a, b);
  a.val = 24;
  std::cout << std::get<0>(t1).val << "\n"; // 42
  std::cout << std::get<1>(t1).val() << "\n"; // 24

  return 0;
}

元组t1中的第二个元素引用自动变量a而不是t1中的第一个元素。有没有办法构造一个元组,以便元组的一个元素可以保存对同一元组中另一个元素的引用?我知道你可以通过创建一个引用元组来实现这个结果,如下所示:

int main()
{
  A a;
  B b(a);
  std::tuple<A &, B &> t2 = std::tie(a, b);
  a.val = 24;
  std::cout << std::get<0>(t2).val << "\n"; // 24
  std::cout << std::get<1>(t2).val() << "\n"; // 24

  return 0;
}

但是为了我的目的,这是作弊,因为t2中的第二个元素仍然最终引用一个生活在元组之外的对象。我能想到的唯一方法是编译好但可能包含未定义的行为[编辑以反映Howard Hinnant提供的更简洁的示例]:

int main()
{
    std::tuple<A, B> t3( A(), B(std::get<0>(t3)) ); // undefined behavior?
    std::get<0>(t3).val = 24;
    std::cout << std::get<0>(t3).val << "\n";
    std::cout << std::get<1>(t3).val() << "\n"; // nasal demons?
}

编辑:这是一个最小的测试程序,当使用带有-O2或更高版本的g ++ 4.7进行编译时返回非零退出状态。这表明未定义的行为或gcc中的错误。

#include <tuple>

class Level1
{
public:
  Level1() : _touched(false), _val(0) { }

  void touch()
  {
    _touched = true;
  }

  double feel()
  {
    if ( _touched )
    {
      _touched = false;
      _val = 42;
    }
    return _val;
  }

private:
  bool _touched;
  double _val;
};

class Level2
{
public:
  Level2(Level1 &level1) : _level1(level1) { }

  double feel()
  {
    return _level1.feel();
  }

private:
  int _spaceholder1;
  double _spaceholder2;
  Level1 &_level1;
};

class Level3
{
public:
  Level3(Level2 &level2) : _level2(level2) { }

  double feel()
  {
    return _level2.feel();
  }

private:
  Level2 &_level2;
};

int main()
{
  std::tuple<Level3, Level2, Level1> levels(
    Level3(std::get<1>(levels)),
    Level2(std::get<2>(levels)),
    Level1()
  );

  std::get<2>(levels).touch();

  return ! ( std::get<0>(levels).feel() > 0 );
}

2 个答案:

答案 0 :(得分:5)

这对我有用:

#include <tuple>
#include <iostream>

int main()
{
    std::tuple<int&, int> t(std::get<1>(t), 2);
    std::cout << std::get<0>(t) << '\n';
    std::get<1>(t) = 3;
    std::cout << std::get<0>(t) << '\n';
}

<强>更新

我刚刚在CWG邮件列表上询问了这个案例。 Mike Miller向我保证,根据3.8p6子弹2,未定义的行为:

  

...如果出现以下情况,程序会有未定义的行为:

     

...

     
      
  • glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或
  •   
     

...

如果tuple是聚合,那么这将是明确定义的行为,但由于tuple具有用户声明的构造函数,因此适用3.8p6b2。

然而,这有效,并避免使用UB:

#include <tuple>
#include <functional>
#include <cassert>

int main()
{
    int dummy;
    std::tuple<std::reference_wrapper<int>, int> t(dummy, 2);
    std::get<0>(t) = std::get<1>(t);
    assert(std::get<0>(t) == 2);
    std::get<1>(t) = 3;
    assert(std::get<0>(t) == 3);
}

答案 1 :(得分:0)

据我所知,为一个尚不存在的对象别名是明确定义的行为。这与您在构造函数初始化列表中使用this所能找到的情况相同,这是不幸但常见且定义明确的。