-1

I am a new C++ developer, and am taking a software engineering class at my university, and the project must be written in C++17, and able to be compiled with MSVC++ 19.

I have had several years of experience with Java and (less so with) C, but the transition to C++ has been... difficult, to say the least, and the comparatively large number of 'pointer'/reference types in C++ has been the crux of the matter.

Long story short, which of these 'pointer'/'reference' types should I be using in my function/method/constructor parameters, return types, and object declarations, and why? My goal is to ensure memory robustness while minimising manual management—similar to Java.


C

In C, things are straightforward: everything is pass-by-value, and there are two types: primitives, and pointers to primitives:

int a = 5; // A is a primitive int
int *ptr = &a; // *ptr dereferences to a, while itself containing the address of a, which the & prefix operator exposes

Given C's pass-by-value paradigm, if one wants to manipulate a variable within a function, a pointer is passed as the argument, and the pointer is then dereferenced within the scope of the function. For memory that needs to be available on the heap, we have malloc/calloc/realloc, and free. The rule is simple here: memory that is malloced to, but not freed by the end of program execution, constitutes a memory leak.


Java

Java makes things even easier, and we have two types, primitives and objects, which are always references:

int a = 5; // primitive int
ArrayList<String> myList = new ArrayList<String>(); // note 'new' keyword; myList's members/methods can be accessed directly with the dot operator such as myList.add("hello")

Memory management is straightforward—albeit slower than C/C++—as objects that go out of scope are automatically cleaned up by the Java garbage collector. No need to worry about deleteing any new objects.


C++

Now, I know for a fact that C++ inherits the & prefix operator as well as the * dereference operator from C. However, there are also the following 'pointer' types:

  • the & post-fix operator: std::string&
  • the set of 'smart pointers':
    • std::auto_ptr (apparently deprecated)
    • std::unique_ptr
    • std::shared_ptr
    • std::weak_ptr

Now, I have seen the following argument types for static/instance functions and constructors (object is a placeholder here; replace with something like string or vector):

  • std::object obj
  • std::object& obj
  • std::object&& obj
  • const std::object obj
  • const std::object& obj
  • the 'smart pointer' types mentioned above

Some of these pointer types are associated with the function (apparently not a function, and doesn't move anything) std::move, but I can't figure out a pattern, based on the C++ code I've seen here and elsewhere on the Web.

I have also seen the term 'copy constructor' and 'destructor', and it has been difficult trying to understand all of these terms and what they do, or how they look like (don't get me started on C++ syntax, which is... painful).

SRSR333
  • 172
  • 1
  • 10
  • 2
    You question, "_Which 'pointer'/'reference' type should I use in C++, and why?_" , doesn't have _one_ answer. It has different answers depending on the situation. – Ted Lyngmo Mar 23 '21 at 13:20
  • The word "reference" in C++ does not mean the same thing as "reference" in Java, and it's better to think of the "smart pointers" as objects with special ownership semantics than as pointers. – molbdnilo Mar 23 '21 at 13:20
  • 3
    Missing from the list `object&&` – Jarod42 Mar 23 '21 at 13:27
  • 1
    Developers coming from languages like Java can sometimes struggle with value semantics. In C++ for a type `T` the expression `T x; T y = x;` creates a copy of `x` where as in Java you would expect `y` to refer to the same object as `x`. It helps to think of all C++ types as Java primitives. This has to be well understood first, otherwise the purpose of all those reference and pointer types is hard to grasp. Note that a lot of those types solve problems that may not be apparent until you learn subtler mechanisms, I wouldn't try to learn them all right away if you are just starting off. – François Andrieux Mar 23 '21 at 13:41

3 Answers3

2

C has two kind of type: objects and functions. All values are objects, some objects are pointers, the value of which is a location of an object or function. Everything is passed by value. Any pointer might be null.

Java has two kinds of type: primitives (that are only defined by the language) and reference to object (the objects can be user defined). Everything is passed by value. Any reference might be null.

C++ has three kinds of type: objects (like C's), functions (like C's) and references (unlike Java's). A reference names an object that exists elsewhere. There is both pass by value and pass by reference.

You use the tool appropriate to the job

  • void frob(type obj) - You only care about the value. A copy exists for the duration of the call. Unlike all the below cases, the runtime type is exactly type, and not a subclass.
  • void frob(type & obj) - You care about the identity, frob may change the value the caller has.
  • void frob(const type & obj) - You care about the identity, frob cannot change the value the caller has. A caller may pass a temporary type, i.e. one that only exists for the duration of the call.
  • void frob(type && obj) - type is expensive to copy, but you can safely steal the innards of one that is about to cease to exist. A caller may pass a temporary type, i.e. one that only exists for the duration of the call.
  • void frob(type * obj) - You care about the identity, frob may change the value, but there might not be an object.
  • void frob(const type * obj) - You care about the identity, frob cannot change the value, but there might not be an object.
  • void frob(std::unique_ptr<type> obj) - You want frob to receive ownership of a dynamically allocated type. When the std::unique_ptr is destroyed, the object (if any) is also destroyed.
  • void frob(std::shared_ptr<type> obj) - You want frob to share ownership of a dynamically allocated type. When the last std::shared_ptr owning an object is destroyed, the object (if any) is also destroyed.

std::auto_ptr is a previous attempt at std::unique_ptr before the language support for stealing expiring values was defined. It shouldn't be used, because "copying" it modifies the source.

As a rule of thumb, most of the time you will be passing by value and returning by value.

When you want a function to be able to modify an argument, most of the time you will pass by reference. When you want to expose a data member, you will return a reference.

If you need a value to outlive it's immediate scope, you will use some form of owning pointer. Almost always this will be std::unique_ptr, which you may need to std::move to it's final location.

All this is possible because C++ has deterministic destruction. When a local variable's scope ends (by any means short of exiting the process), it's destructor runs. This is similar to Java's try-with-resources, but built into everything.

Caleth
  • 35,377
  • 2
  • 31
  • 53
1

The bottom line is that you are missing some basic knowledge in C++ - about passing values and references around - which typically comes from a textbook or an introductory course. I'm guessing this is also why some believe your question should be closed (and perhaps it should, I'm not sure).

But I don't want to shirk your question completely, so here are some imperfect and incomplete rules of thumb:

  • By default, and unless you have reason to - don't use references. Use plain values and let the compiler sort things out.
  • Prefer using references (&) over pointers.
  • When you want memory which lasts beyond your current scope, consider using a smart pointer. This is an explanation about what those are, including std::shared_ptr and std::unique_ptr, and how to use them.
  • Use references or pointers in function parameters when you know you need to take in references or pointers; and in those cases
  • rvalue references (e.g. with &&'s) have to do with move semantics. You should read about that.
einpoklum
  • 86,754
  • 39
  • 223
  • 453
-2

Since you're familiar with Java, you could write Java-like code in C++ which might make the transition easier. One piece of this would be to traffic in std::shared_ptr which will get you similar behavior to Java's references (although through an entirely different mechanism).

std::shared_ptr is a good way of managing memory in C++ and it feels like Java (no worrying about delete, etc.) (An "excellent" way would be to use a higher-level construct like std::optional or std::vector; or a more restrictive std::unique_ptr.)

For example, to create a class Foo:

struct Foo_ final
{
   Foo_() = default;
   Foo_(const Foo_&) = delete;
   Foo_& operator=(const Foo_&) = delete;

   int i = 314;
};
using Foo = std::shared_ptr<Foo_>;

Then, later in your code

void doSomething(Foo foo)
{ /* ... */ }

Foo foo = std::make_shared<Foo_>();
doSomething(foo);

Eventually, you'll probably want to do things the "proper" C++ way; and your fellow C++ developers might scream at you for code like the above.


( I've often thought that if we had std::shared_ptr long before C++11, we might never have seen Java or C#. )

Ðаn
  • 10,400
  • 11
  • 57
  • 90
  • 2
    I don't see the point in first learning completely wrong idioms and then relearning the right ones afterwards. C++ is neither C nor Java and you shouldn't try to press it into that schema. (Also no there's a lot of horrible things in C++ not in Java, shared pointers only address a tiny part of the problem). – Voo Mar 23 '21 at 14:38
  • 1
    @Dan So instead of learning how C++ structures and idioms work, they should write Java-like C++ code using shared_pointer. And then some time in the future try and get rid of all the bad practices they've learned and relearn them the right way. Which is much harder than learning the right thing from the get-go. It's well known how long it takes to unlearn bad behavior. – Voo Mar 23 '21 at 14:42
  • 1
    @Dan I would say it's hard to do worse than not understanding how ownership and the various pointer libraries, as well as RAII, work in C++. But you yourself admit that this is not idiomatic C++ code. Hence instead of learning the right way now, learn the wrong way and then spend 10 times (that's one common estimate) longer relearning the right way later. – Voo Mar 23 '21 at 14:44