Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can one element of a tuple reference another?

Update: See the full answer below. The short answer is no, not directly. You can create an indirect reference using std::reference_wrapper or accomplish the same effect more generally with pointers (but without the syntactic sugar and added safety of references).

I ask because tuples make a convenient variadic storage unit in C++11. In theory it sounds reasonable for one element of a tuple to hold a reference to another element in the same tuple. (Replace "reference" with "pointer" and it works in practice.) The devil is the details of constructing such a tuple. Consider the following example:

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

The second element in the tuple t1 references the automatic variable a instead of the first element in t1. Is there any way to construct a tuple such that one element of the tuple could hold a reference to another element in the same tuple? I'm aware that you could sort of achieve this result by creating a tuple of references, like this:

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

But for my purposes that's cheating, since the second element in t2 is still ultimately referencing an object that lives outside of the tuple. The only way I can think of doing it compiles fine but may contain undefined behavior [Edited to reflect more concise example provided by 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?
}

Edit: Here is a minimal test program that returns with a non-zero exit status when compiled using g++ 4.7 with -O2 or higher. This suggests either undefined behavior or a bug in 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 );
}
like image 828
Benjamin Kay Avatar asked Jul 26 '12 18:07

Benjamin Kay


1 Answers

This works for me:

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

Update

I just asked about this case on the CWG mailing list. Mike Miller assures me that this is undefined behavior per 3.8p6 bullet 2:

... The program has undefined behavior if:

...

  • the glvalue is used to access a non-static data member or call a non-static member function of the object, or

...

It would be well defined behavior if tuple was an aggregate, but because tuple has a user-declared constructor, 3.8p6b2 applies.

However this works, and avoids 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);
}
like image 83
Howard Hinnant Avatar answered Oct 15 '22 14:10

Howard Hinnant