0

I'm looking at the following tutorial: http://www.videotutorialsrock.com/opengl_tutorial/animation/home.php

This person has a vector class:

class Vec3f {
private:
    float v[3];
public:
    Vec3f();
    Vec3f(float x, float y, float z);

    float &operator[](int index);
    float operator[](int index) const;

    Vec3f operator*(float scale) const;
    Vec3f operator/(float scale) const;
    Vec3f operator+(const Vec3f &other) const;
    Vec3f operator-(const Vec3f &other) const;
    Vec3f operator-() const;

    const Vec3f &operator*=(float scale);
    const Vec3f &operator/=(float scale);
    const Vec3f &operator+=(const Vec3f &other);
    const Vec3f &operator-=(const Vec3f &other);

    float magnitude() const;
    float magnitudeSquared() const;
    Vec3f normalize() const;
    float dot(const Vec3f &other) const;
    Vec3f cross(const Vec3f &other) const;
};

With an example definition:

Vec3f Vec3f::operator*(float scale) const {
    return Vec3f(v[0] * scale, v[1] * scale, v[2] * scale);
}

I'm confused on why this works. Shouldn't this immediately cause a segmentation fault? The return value is on the stack and should get deleted upon termination of all of these functions. Why does it work? Is my understanding of stack vs heap incorrect?

EDIT: I am basing my understanding from this: How to return a class object by reference in C++?

lee
  • 740
  • 2
  • 11
  • 24
  • 1
    If return values got destroyed before the function exited, what would be the point of return values? – alter_igel Mar 07 '19 at 22:47
  • Possible duplicate of [What are rvalues, lvalues, xvalues, glvalues, and prvalues?](https://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues) – Tzalumen Mar 07 '19 at 22:48
  • Do you understand how this works: `int foo() { int i = 7; return i; }` ? – Eljay Mar 07 '19 at 22:48
  • @Eljay sure, but here we're returning a class. https://stackoverflow.com/questions/8914509/how-to-return-a-class-object-by-reference-in-c – lee Mar 07 '19 at 22:49
  • 1
    But it's not being returned by reference. This is a plain old return by value. The object is being copied and it is trivially copyable so there's not much that can go wrong. Plus there is a lot of room for the compiler to eliminate the copy (See Copy Elision). – user4581301 Mar 07 '19 at 22:51
  • 2
    return by reference != return by copy/value. If you were returning by reference, you would indeed have undefined behavior (UB). – Jarod42 Mar 07 '19 at 22:51
  • @Jarod42 So if Vec3f had dynamic memory allocated on the heap, if I define my copy constructor, will this be safe? – lee Mar 07 '19 at 22:53
  • 2
    @SpentDeath if your `Vec3f` had dynamically allocated member data, and followed the [Rule of Three](https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming)), then yes, it would be perfectly safe. – alter_igel Mar 07 '19 at 22:54
  • Some good supplemental reading on heap vs. stack: https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap – alter_igel Mar 07 '19 at 22:56
  • 2
    But if you do **not** follow the Rule of Three, then you are very likely smurfed one way or another. You have a memory leak or sooner or later there is a double delete. Your goal as a programmer should be to design your classes to observe the Rule of zero or hide all of the management through the Rules of Three and Five from users to that they can adhere to the Rule of Zero. – user4581301 Mar 07 '19 at 22:57
  • 1
    @user4581301 thank you for this lovely new expletive – alter_igel Mar 07 '19 at 23:11

4 Answers4

4
Vec3f Vec3f::operator*(float scale) const {
    return Vec3f(v[0] * scale, v[1] * scale, v[2] * scale);
}

This uses return by value, so what is returned is the value of the class instance created by that line, not the instance itself.

This is fundamentally no different from return 1;. The value one is returned, not any particular instance or class member that contains that value. As with pretty much everything else, it's the implementation's responsibility to figure out how to accomplish what the code asks for -- in this case, ensuring that some instance exists to hold the returned value with an appropriate lifetime.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
1

You can look in the following example:

Vec3f Vec3f::operator*(float scale) const {
    return Vec3f(v[0] * scale, v[1] * scale, v[2] * scale);
}

Vec3f a(1,2,3);
Vec3f b;
b = a * 2;

In general the following will happen:

  1. the operator overload implementation will construct a new instance of the Ve3f with the new arguments, representing multiplication.

  2. the return procedure will call a default copy constructor of b with the constructed object in the argument. The copy-constructor will copy fields from its argument to the instance of 'b'.

You can always implement your own copy constructor to do something else besides the shallow copy which the default one provides.

Vec3f(const Ver3f &src)...

So, as a result you will get a new objects with all fields copied from the one created at the return statement. This is the return by value as defined in c++ for objects.

The result would be different if you returned the object by a pointer or a reference. It will cause a memory corruption.

Serge
  • 11,616
  • 3
  • 18
  • 28
  • *You can always implement your own copy constructor to do something else besides the shallow copy which the default one provides* -- For the OP's case, the `default` versions of the copy constructor, assignment operator, and destructor are perfectly fine, since the only member is an array of 3 float's. Writing versions of these functions *when there is no need to* can lead to bugs or code that may not be optimized. – PaulMcKenzie Mar 08 '19 at 04:13
0

That's the binary multiplication operator, and it's returning a copy of the data in the Vec3f instance, multiplied by float scale, into an rvalue for use by the rest of an expression.

How that works is already answered in What are rvalues, lvalues, xvalues, glvalues, and prvalues?

Also see https://en.cppreference.com/w/cpp/language/operator_arithmetic

Tzalumen
  • 652
  • 3
  • 16
  • 1
    By dereference operator, do you mean the unary `operator*`? OP's function clearly uses a `float scale` argument, which makes this the infix `operator*`. Also, I really don't think value categories shed any light on how return values work with the stack. – alter_igel Mar 07 '19 at 22:49
  • Oh, interesting. You're right, I missed that. I'll change the operator mentioned. That said, it does return an rvalue, and understanding how return values work is still best covered by the other question, and is probably a useful read for OP. I did mark this as a duplicate. – Tzalumen Mar 07 '19 at 22:53
-1

So, every cpu has it's own calling convention. For more detailed info look into this and this

Basically, the return value, or the address to the return value is copied to the a register like R0 in ARM and EAX in x86 so the caller of the function can access it.

Moe
  • 39
  • 1
  • 9
  • Whatever calling convention is being used is an implementation detail of the compiler. That's beyond the scope of the rules of the C++ language and has nothing to do with what a program means in terms of the language. – alter_igel Mar 07 '19 at 23:00
  • even if the code is in C++, the question is general and about heap and stack. they're generally wondering how values are returned and told them how. – Moe Mar 07 '19 at 23:05
  • Then can you explain how the calling convention distinguishes returning by value versus returning by reference in C++, and what the calling convention does to ensure the existence of the returned object? – alter_igel Mar 07 '19 at 23:12