About const & arguments (again)

I am working on a family of generalized container wrappers and sequence generators that provide the classic sequence facilities such as integer range, mapping of a function to a sequence, trimming or reversing of a sequence, etc ..

Each class wraps a sequence and exposes a modified iterator. They all have a structure similar to the boxA and boxB in the attached code.

The difference between BoxA and BoxB is that BoxA stores a const & to the underlying container or generator while BoxB stores a non-const reference.

My first question is why is it possible to use nested expressions with the const version, while it is necessary to store the output of calls to the boxB constructor in a variable

1
2
3
4
auto x = make_boxA(make_boxA(v));  //this is not possible with BoxB;

auto y1 = boxB(v);  //this what is required with boxB
auto y2 = boxB(y1);


Secondly, I really like the aesthetics and simplicity of the nested expressions; is there a way to make them possible while avoiding the const’ing of the underlying container reference?

The problem when the underlying container is const’ed is that ... it is const'ed and the class loses the ability to expose direct references to the content of the underlying object. For example operator [] and the dereferenced iterator can’t be used as an l-value. Obviously, such an assignment doesn't always make sense depending on the type of wrapped sequence/container but that can be handled effectively through template parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

#include <iostream>
#include <vector>

template < class T > class boxA
{
public:
  const T & v;
  typedef decltype(v[std::declval<int>()]) R;
  typedef decltype(v.begin()) I; 
  boxA (const T & v):v (v) {}
  R operator[] (int i) const{return v[i];}
  I begin()  const {return v.begin();}
  I end() const {return v.end();}
};

template < class T > 
boxA < T > make_boxA (const T & b)
{return boxA < T > (b);}


template < class T > class boxB
{
public:
  T & v;
  typedef decltype(v[std::declval<int>()]) R;
  typedef decltype(v.begin()) I;
  boxB ( T & v):v (v) {}
  R operator[] (int i) const {return v[i];}
  R & operator[] (int i) {return v[i];}
  I begin(){return v.begin();}
  I end(){return v.end();}
};

template < class T > 
boxB < T > make_boxB ( T & b)
{return boxB < T > (b);}



int main ()
{
  std::vector < int >v = { 1, 2, 3, 4, 5, 6 };
//with boxA
  for(auto i : make_boxA (make_boxA (v))){
  std::cout << i <<", ";     }
  std::cout  << std::endl;

//with boxB
  auto c = make_boxB (v);
  auto d = make_boxB (c);
  d[3] = 99;
  for(auto i : d){std::cout << i <<", "; }
  std::cout  << std::endl;
}

Last edited on
According to the code you've posted, this:
 
auto x = make_boxA(make_boxA(v));
creates a boxA whose v member is an invalid reference. It points to a temporary that no longer exists. The correct way to construct a boxA is exactly what you do for boxB.
That's interesting! I am working on Cloud9 and it works perfectly. I also tested the code successfully on onlinegdb.com and also on cpp.sh from the link next to the code box above.

Also, base on this reference :
https://www.learncpp.com/cpp-tutorial/73-passing-arguments-by-reference/

I understand that the code is valid. The article says "Const references can accept any type of argument, including l-values, const l-values, and r-values. "

In this case make_box(v) is an r-value and so it is a valid argument to the outer function call in make_boxA(make_boxA(v)) and the whole expression is also an r-value that valid on the right-hand side of the assignment.

It is clear the object needs to be stored, I guess on the stack. I am still not clear why a non-constant reference could not work,




Last edited on
I had assumed the const version worked because the compiler was able to streamline the expression and remove the constructor calls, thus removing the need for temp storage.
That's not possible. Inside the boxA constructor the reference parameter has to point somewhere, and you have to set the member reference to point to something. If we assume that no temporary boxA has been constructed, then what's the member reference pointing to after the constructor returns?

The point of undefined behavior is that all behaviors are permissible. A program that generates correct results may still contain undefined behavior.
Last edited on
Thanks for your response.
I think I found the explanation here:

https://en.cppreference.com/w/cpp/language/lifetime

The key sentences are

"a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call"
and
"a temporary bound to a reference member in a constructor initializer list persists only until the constructor exits, not as long as the object exists.:


So the inner object is maintained alive until the outer function exits, so the outer object is properly constructed but its reference to the inner object is not safe once the evaluation of the whole expression is completed.

Probably, I was lucky in my tests and the memory was not overwritten. Well, will modify my code and use heap allocation objects and smart pointers instead!

Thanks for your help.

more reference articles
https://stackoverflow.com/questions/2784262/does-a-const-reference-class-member-prolong-the-life-of-a-temporary
https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/

Last edited on
Topic archived. No new replies allowed.