When is a declaration a reference?

My boss reviewed some code I wrote that used a list of structs:

typedef struct {...} MY_STRUCT_T;
std::list<MY_STRUCT_T *> myList;

void funcA()
{
MY_STRUCT_T *myStruct = new MY_STRUCT_T(...);
myList.push_back(myStruct);
}

void funcB()
{
MY_STRUCT_T *myStruct = myList.front();

// ...access myStruct data members here...

delete *myStruct;
myList.pop_front();
}


He preferred that I write:

typedef struct {...} HIS_STRUCT_T;
std::list<HIS_STRUCT_T> hisList;

void funcA()
{
MY_STRUCT_T myStruct = myList.front();
hisList.push_back(hisStruct);
}

void funcB()
{
HIS_STRUCT_T hisStruct(...);

// ...access hisStruct data members here...

hisList.pop_front();
}

I said that hisStruct was an automatic data variable created on the stack and that it would go out of scope and be destroyed when the function returned. He said that classes and structs declared inside a function were created on the heap and were actually references, so they would not get destroyed until the reference count went to 0. To which I replied that hisStruct was not a reference because it was not declared using the '&' symbol.

So I rewrote his way, tested it, and was surprised that it worked! I thought I would get a bus trap, exception, or some other bad behavior.

What's going on?
I said that hisStruct was an automatic data variable created on the stack and that it would go out of scope and be destroyed when the function returned.

That is correct.

He said that classes and structs declared inside a function were created on the heap and were actually references, so they would not get destroyed until the reference count went to 0.

That is not correct.

However, the push_back function dynamically allocates memory for the object you insert on the list, you don't have to do it yourself. That's the reason the code suggested by your boss works. His explanation has nothing to do with it and neither is correct anyway.

EDIT: I want you to also notice that neither would your approach work if push_back didn't dynamically allocate memory. You see, the pointer you store is also a temporary variable and will be destroyed when your function returns. If push_back didn't dynamically allocate memory, and a list held references to objects instead of actual objects, you'd have to do something like this:

1
2
3
4
5
6
7
8
typedef struct {...} MY_STRUCT_T;
std::list<MY_STRUCT_T> myList;

void funcA()
{
     MY_STRUCT_T *myStruct = new MY_STRUCT_T(...);
     myList.push_back(*myStruct);
}
Last edited on
What happens is that hisStruct is copied, just like your myStruct pointer is copied into the list, so it doesn't matter that the local pointer/struct is destroyed at the end of the function.

References don't count anything in C++, they're just pointers with different syntax.
Maybe your boss was thinking of Java.
Last edited on
He said that classes and structs declared inside a function were created on the heap and were actually references, so they would not get destroyed until the reference count went to 0.
Is that literally what he said? Because that's one of the most retarded things I've ever heard.
Looking at this code, I can imagine that what he actually said was that the *_STRUCT_T instances were created inside list nodes that are allocated on the heap, and that the list that keeps these nodes from being destructed will exist as long as the program doesn't terminate.
In any case, yours may be more efficient, depending on how large a single *_STRUCT_T object is. I would possibly rewrite it like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Note: Your identifiers seemed a bit schizophrenic on your second snippet. I'm
//assumming MY_STRUCT_T==HIS_STRUCT_T and myList==hisList.

void funcA(){
	//2 copies being performed:
	//MY_STRUCT_T myStruct = myList.front();
	//myList.push_back(myStruct);
	
	//No copies performed. Only a single object is constructed:
	myList.push_back(MY_STRUCT_T());
}

void funcB(){
	//Yet another useless copy:
	//HIS_STRUCT_T myStruct=myList.front();

	MY_STRUCT_T &myStruct=myList.front();
	//...
	myList.pop_front();
}


EDIT:
m4ster r0shi wrote:
if push_back didn't dynamically allocate memory
You can't implement a linked list without dynamic allocation. I'm not sure what difference your snippet makes other than leak memory.
Last edited on
Wait, I'll post an example of what I mean shortly...
1
2
//No copies performed. Only a single object is constructed:
	myList.push_back(MY_STRUCT_T());

Not quite correct, the object still needs to be copied once.
Damn. You're right. I overestimated the smartness of the compiler. Not even list.resize(list.size()+1) calls a single constructor. Maybe MinGW is just being stupid.
Ok, that's what I meant. Of course, you need to at least allocate memory for the pointers to the stored objects, but you can implement a list that doesn't actually create objects.

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include<iostream>
using namespace std;

template <class T>
class List
{
    template <class A>
    struct Node
    {
        A * ptr;
        Node * next;
    };

    Node<T> * first;
    Node<T> * last;

    int size;

public:

    List():first(0),last(0),size(0){}
    ~List();

    void push(T & t);
    T & pop();
};

template <class T>
List<T>::~List()
{
    Node<T> * cur=first;
    while (cur)
    {
        first=first->next;
        delete cur;
        cur=first;
    }
}

template <class T>
void List<T>::push(T & t)
{
    size++;

    cout << "pushing " << t << endl;

    if (!first)
    {
        first=new Node<T>;
        first->ptr=&t;
        first->next=0;
        last=first;
        return;
    }

    Node<T> * new_node=new Node<T>;
    new_node->ptr=&t;
    new_node->next=0;
    last->next=new_node;
    last=new_node;
}

template <class T>
T & List<T>::pop()
{
    if (size==0) throw("nothing to pop");

    size--;

    T & ret=*(last->ptr);

    if (size==0)
    {
        delete first;
        last=first=0;
        return ret;
    }

    Node<T> * prev=first;
    while (prev->next!=last)
        prev=prev->next;

    delete last;
    last=prev;
    last->next=0;

    return ret;
}

int main()
{
    int * n1=new int(1);
    int * n2=new int(2);
    int * n3=new int(3);

    List<int> int_list;

    int_list.push(*n1);
    int_list.push(*n2);
    int_list.push(*n3);

    cout << "poping " << int_list.pop() << endl;
    cout << "poping " << int_list.pop() << endl;
    cout << "poping " << int_list.pop() << endl;

    delete n1;
    delete n2;
    delete n3;
}
Last edited on
Damn. You're right. I overestimated the smartness of the compiler. Not even list.resize(list.size()+1) calls a single constructor. Maybe MinGW is just being stupid.

Hm well, the compiler is not allowed to omit the copy. The only case where this is allowed by the C++ standard are function return values.
Still, for all compiler optimizations the same rule applies: as long as it doesn't change the behaviour of the program, everything is allowed. So that just means that any side effects that the copy constructor might have must be carried out.

So in this simple case:
1
2
3
typedef pair<int,int> int_pair;
list<int_pair> blah;
blah.push_back(int_pair(555,999));


gcc can call new to allocate a new list node and can then move the two numbers directly into the node. Formally, a copy of the temporary pair is constructed inside the node. But when optimizing, the temporary object just exists on paper, not in the actual code.
Rule #1, don't argue with the boss.
Rule #2, if you do have to argue with the boss, sack him and employ a better boss.
Topic archived. No new replies allowed.