Inheritance References not working correctly

Viewed 125

When running the following code, which involves changing the value of a int via a method of a class called B, which inherits from a templated class A, the value of the int doesn't change, and I cannot understand why, I have tested both of these with clang trunk and gcc trunk:

#include <iostream>

template<typename T>
struct A
{
    A(T& a_num_) : a_num(a_num_) {}

    T& a_num;
};

struct B : public A<int>
{
    template<typename... Args>
    B(Args... args) : A<int>(args...) {}

    void do_something()
    {
        a_num = 1634;
    }
};

int main(void)
{
    int num = 4;

    B b {num};
    b.do_something();

    std::cout << num;

    return 0;
}

I would expect to get 1634 printed, but instead 4 prints.

I have narrowed the error to the constructor of B, because if I change the constructor of B to be:

B(int& x) : A<int>(x) {}

then the correct value appears, but the current constructor should also be receiving an int, because when I type:

B b {num};

Then it should choose Args = [int&], because that's the only way to satisfy A's constructor, which takes a T&, so what is happening here? What is a_num in this case? Just a garbage reference that doesn't reference anything, or possibly a temporary object?

I've also tried to rewrite the do_something function as

A<int>::a_num = 1634

but it still doesn't manage to change it.

I've also noticed that declaring b as:

B b {6};

Also works, although there's no way 6, which is a pr-value, could bind to a l-value reference.

So my question here is why does B's constructor choose Args = [int] instead of Args = [int&] and how can it do this when it then passes that Args to a constructor that takes a T&?

2 Answers

The only time a template type parameter will be deduced to be a reference type is when you declare the corresponding function parameter to be T&& and pass an lvalue to the function.

That means in this case Args is deduced to be {int}. After B's constructor is instantiated, it looks like this:

B(int arg) : A<int>(arg) {}

This is perfectly valid, because arg is an lvalue, and so the a_num_ parameter to A's constructor can bind to it. You then copy that reference to the A's a_num member and then the function parameter it's bound to goes out of scope and a_num becomes a dangling reference. The behavior when you attempt to assign through a_num is therefore undefined.


If you want B's constructor to take its arguments by reference, then you need to define the argument's type to be a reference:

tmeplate <typename... Args>
B(Args&... args) : A<int>(args) {}

If you do that then b.a_num will be a reference to the num you defined in main and everything will work like you expect.

You should change B's constructor to take Args as a reference Args&...:

template<typename... Args>
B(Args&... args) : A<int>(args...) {}

Now your code will print 1634 as you originally wanted.

Confirming the memory addresses

If you want to confirm that those variables point to the same memory address, you can do:

std::cout << &num << '\n';
std::cout << &b.a_num << '\n';

This prints the same memory address. For example:

0x7fff5508cac8
0x7fff5508cac8

Using pointers

Since the idea is to make both num and A::a_num point to the same memory address, it is worth mentioning a solution that uses pointers. In that case, you don't have to change B's constructor, but you have to change A::a_num to be a T*.

template<typename T>
struct A
{
    A(T* a_num_) : a_num(a_num_) {}

    T* a_num;
};

struct B : public A<int>
{
    template<typename... Args>
    B(Args... args) : A<int>(args...) {}

    void do_something() {
        *a_num = 1634;
    }
};

int main(void)
{
    int num = 4;

    B b {&num};
    b.do_something();

    std::cout << num;
}

This code also prints 1634 as you originally wanted.

Edit: I should remember that this code is bad practice and should never be recommended for production use. The memory address of local variables should never be passed around since they are only valid while the scope exists in the stack. We can implement much better code by using smart pointers or simply by copying the values instead of reusing their memory addresses.

Related