0

I'm researching the differences between stack and heap allocation, which has its own long list of disputes on when to use which. My question is not related to that discussion though; it is about the semantics of testing these two ideas.

Consider the following situation describing the usual ways variables are passed between functions:

void passByPtr(Node* n)
{n->value = 1;}
void passByRef(Node& n)
{n.value = 2;}
void passByValue(Node n)
{n.value = 3;}
void indirectionTesting()
{
    Node* heapNode = new Node();
    Node stackNode= Node();

    // Initialize values
    heapNode->value = 0, stackNode.value = 0;
    cout << heapNode->value << ", "<< stackNode.value << endl;

    // Passed ptr by ptr (pass by reference)
    passByPtr(heapNode);
    passByPtr(&stackNode);
    cout << heapNode->value  << ", "<< stackNode.value << endl;
    // Pass by reference
    passByRef(*heapNode);
    passByRef(stackNode);
    cout << heapNode->value << ", "<< stackNode.value << endl;
    // Pass by value
    passByValue(*heapNode);
    passByValue(stackNode);
    cout << heapNode->value  << ", "<< stackNode.value << endl;

    delete heapNode; 
}
int main()
{
    indirectionTesting();
    /* Output  
       0, 0
       1, 1
       2, 2
       2, 2  */
    return 0;
}

These are the two ways I was taught how to handle variable passing between methods. There are a lot of arguments for and against here. I read all of them.

But I had an idea... you see, I'd prefer for the caller to be able to easily out whether or not a function is conceptually pass-by-value or pass-by-reference. (i.e. (func(&var)) In my humble opinion, the transparency supplements the idea of encapsulation.

But there's a problem. The 'new' keyword always returns a pointer. As you can tell from the above example, if you use a mix of stack-allocated and heap-allocated variables in your program, it can quickly become confusing how to pass them by reference.

So after playing around with reference types, now consider this portion of code:

void passByPtr(Node* n)
{n->value = 1;}
void passByRef(Node& n)
{n.value = 2;}
void passByValue(Node n)
{n.value = 3;}
void indirectionTesting()
{
    Node& heapNode = *new Node();
    Node stackNode= Node();

    // Initialize values
    heapNode.value = 0, stackNode.value = 0;
    cout << heapNode.value << ", "<< stackNode.value << endl;

    // Passed ptr by ptr (pass by reference)
    passByPtr(&heapNode);
    passByPtr(&stackNode);
    cout << heapNode.value << ", "<< stackNode.value << endl;
    // Pass by reference
    passByRef(heapNode);
    passByRef(stackNode);
    cout << heapNode.value << ", "<< stackNode.value << endl;
    // Pass by value
    passByValue(heapNode);
    passByValue(stackNode);
    cout << heapNode.value << ", "<< stackNode.value << endl;

    delete &heapNode;
}
int main()
{
    indirectionTesting();
    /* Output  
       0, 0
       1, 1
       2, 2
       2, 2  */ 
    return 0;
}

Ignoring how strange the heap-allocated variable instantiation looks, does anyone see anything wrong with this?

They both function exactly the same. The way I see it, choosing to follow this type of convention allows me to freely choose whether to allocate on the stack or heap whilst allowing me to use the same calling convention for all of my functions, again regardless whether or not they are heap or stack allocated. I don't personally see any validity in the argument that "I will confuse which variables were allocated on the stack and which on the heap," but share your opinion if you think I'm wrong.

Does anyone see anything else wrong with this approach?

Community
  • 1
  • 1
  • 1
    You can pass objects by pointer, reference or value regardless of whether they were allocated by new or placed on the stack. Unless the function is going to keep a persistent reference of the object somewhere, it shouldn't care how it was allocated. Therefore, I don't understand your question. My biggest concern is your memory leaks. – Neil Kirk Mar 23 '15 at 20:59
  • *I don't personally see any validity in the argument that "I will confuse which variables were allocated on the stack and which on the heap,"* Ironically, that's your reason for doing this in the first place, confusing which are references or pointers. – Neil Kirk Mar 23 '15 at 21:02
  • Fixed to address memory leaks. –  Mar 23 '15 at 21:02
  • `delete heapNode;` doesn't work with a reference. It only takes a pointer ;) – Neil Kirk Mar 23 '15 at 21:03
  • This was a toy problem. –  Mar 23 '15 at 21:03
  • @Neil: Trivial to obtain. `delete &ref` – Lightness Races in Orbit Mar 23 '15 at 21:03
  • What is the real problem? – Neil Kirk Mar 23 '15 at 21:04
  • I wanted to ask if there were logically any problems that people noticed –  Mar 23 '15 at 21:08
  • What does allocating on the "heap" vs. the "stack have to do with pass by pointer / pass by reference? – juanchopanza Mar 23 '15 at 21:36

3 Answers3

0

I recommend passing by reference to functions. A reference indicates that the object exists.

When passing by pointer, the receiving function doesn't know if the pointer is valid or not and must test the pointer.

The passing by reference is a safer and more robust style.

Thomas Matthews
  • 56,849
  • 17
  • 98
  • 154
  • Passing by references isn't safer than passing by pointers, although it helps to self-document the code. – Neil Kirk Mar 23 '15 at 21:12
  • I can pass a null pointer. I can have a pointer's value point to an invalid address in the memory space. This cannot happen when passing by reference. – Thomas Matthews Mar 23 '15 at 21:13
  • @ThomasMatthews: This is not _supposed_ to happen when passing by reference. It's trivially done with undefined behaviors though, which are _possible_. – Mooing Duck Mar 23 '15 at 21:14
  • `T *p = NULL; myfunc(*p);` Done. It's illegal, but you did say "safer", which I take in the context of everyday code-writing. Using references instead of pointers offers no magic protection against passing invalid addresses. – Neil Kirk Mar 23 '15 at 21:15
0

pass by pointer as last resort.

the best option - pass by reference as much as possible. references prevents you passing null pointers and dangling pointers. it also prevent you doing the bad bad practice of passing null as "no valid input".

there are also places you MUST pass by const reference, like in any function that gets temporary object as parameter, or copy constructors

also, every object that weight more than sizeof(void*) can benefit by passing it by reference - the reference pass will cost less byte-copying thus a faster program. pass heavy objects as references if they need to change and as const references if they do not.

if you can't pass by reference , try pass by smart pointers - std::weak_ptr and std::shared_ptr (you cant pass std::unique_ptr, or at least, you shouldn't).

you pass by raw pointer when :

  1. you can't pass by reference, for instance like passing this
  2. null is a legit argument
  3. C-API's

you don't pass either by pointer nor reference if:

  • you do want your object to be copied
  • there is no performance improvement. for example, I pass a character and I don't want it to be changed, yet , passing it as const reference makes the program slower (references are usually implemented behind the scenes as pointers, and they weight many times heavier than single character)

few more things:

  1. you CAN catch a new result with reference, just dereference it first: int& x = *new int(4)
  2. don't use new/delete unless you're dealing with performance. use smart pointers instead (std::shared_ptr,std::unique_ptr).
  3. even if you do have a pointer as variable (for example, if you imlement linked list or singleton and you use raw pointers) , you can still and should return it as reference:

    T& T::getInstance(){ if (!m_Instance){ m_Instance = new T(); } return *m_Instance; }

and again , the above code CAN AND SHOULD be used with smart pointers

David Haim
  • 25,446
  • 3
  • 44
  • 78
  • How do references *prevent* passing null and dangling pointers? `T *p = NULL; myfunc(*p);` Whoops! – Neil Kirk Mar 23 '15 at 21:17
  • why would someone write such code in the first place? as Strauatrop said himself , C++ prevents mistakes , not fraud. the language can't protect you from yourself. plus , technically , your not passing null-pointer per se, you dereference it before you pass it. whoops. – David Haim Mar 23 '15 at 21:18
  • That's a simple example. In non-trivial code, you can have a pointer whose value is null but not supposed to be, and deference it for a reference parameter. The mere use of a reference offers no extra protection over just passing the pointer for preventing bugs. – Neil Kirk Mar 23 '15 at 21:22
0

If you really want to do this:

  • Have the caller determine whether the parameter is by ref, value or ptr
  • Be able to swap object types between stack and heap

Then I think you might be able to do something along these lines:

auto my_node = make_object<Node>(); // factory hides value/heap creation

Then you'd make every object created be wrapped by a class which has pointer semantics. So if my_node is on the heap, it's wrapped so everything uses my_node-> or *my_node (even though the wrapper is just storing it as a member variable).

Since you are now working with wrapped types, a function might have a signature like:

value<Node> my_function( parameter<Node> );

I haven't thought through the details of how these classes might operate but I was thinking something along the lines of:

parameter can only be constructed from a reference, value or ptr. So the caller then specifies which of these it wants and some mechanics under the covers deal with getting the parameter through in the correct form.

// in the function with the my_node object
auto ret = my_function( pass_by_reference(my_node) );

I'm sure there will be some tricky bits in the implementation but on a surface level I think something along these lines would let you overload the make_object so different object types are associated with heap/stack as desired.

You wouldn't be able to work with anything like it is a reference - you'd have to deal with pointer syntax because its the lowest common denominator. However you could retain the semantics of something being a reference or a pointer (can be made null or rebound) or a value (on the stack).

Clearly there would be some inefficiency somewhere because the generic 'parameter' type would need to be able to operate in 3 different 'modes'.

But you know - you are trying to make the language do something it wasn't designed to. However the strength of C++ is that where theres a will, theres a way.

Maybe someone will tell me why the above will not work. I'd be happy to hear that but on a cursory consideration I think it could be made to work.

qeadz
  • 1,476
  • 1
  • 9
  • 17