0

I've just begun learning C++ and I wanted to get my head around the different ways to create variables and what the different keywords mean. I couldn't find any description that really went through it, so I wrote this to try to understand what's going on. Have I missed anything? Am I wrong about anything?

Global Variables

Global variables are stored neither on the heap nor stack. static global variables are non-exported (standard global variables can be accessed with extern, static globals cannot)

Dynamic Variables

Any variable that is accessed with a pointer is stored on the heap. Heap variables are allocated with the new keyword, which returns a pointer to the memory address on the heap. The pointer itself is a standard stack variable.

Variables inside {} that aren't created with new

Stored in the stack, which is limited in size so it should be used only for primitives and small data structures. static keyword means the variable is essentially global and stored in the same memory space as global variables, but scope is restricted to this function/class. const keyword means you can't change the variable. thread_local is like static but each thread gets its own variable.

Register

A variable can be declared as register to hint to the compiler that it should be stored in the register. The compiler will probaby ignore this and apply it to whatever it thinks would be the best improvement. Typical usage would be for an index or pointer being used as an interator in a loop.

Good practice

  1. Use const by default when applicable, its faster.
  2. Be wary of static and globals in multithreaded applications, instead use thread_local or mutex
  3. Use register on iterators

Notes

Any variables created inside a function (non-global) that is not static or thread_local and is not created with new, will be on the stack. Stack variables should not exceed more than a few KB in memory, otherwise use new to put it on the heap.

The full available system memory can be used for variables with static keyword, thread_local keyword, created with new, or global.

Variables created with new need to be freed with delete. All others are automatically freed when they're out of scope, except static, thead_local and globals which are freed when the program ends.

Despite all the parroting about how globals should not be used, don't be bullied: they are great for some use cases, and more efficient than variables allocated on the heap. Mutexes will be needed to avoid race conditions in multi-threaded applications.

Alasdair
  • 11,936
  • 14
  • 66
  • 125
  • 1
    https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list – 463035818_is_not_a_number Mar 17 '21 at 10:40
  • don't confuse dynamic allocation to be the same as using `new` in your code. In modern C++ you should rarely if ever use naked `new` to allocate something dynamically – 463035818_is_not_a_number Mar 17 '21 at 10:43
  • @largest_prime_is_463035818 without `new` how would I allocate a really large array? If I did it without `new` it'd cause stack overflow. – Alasdair Mar 17 '21 at 10:45
  • 4
    `std::vector x( very_large_numer);` – 463035818_is_not_a_number Mar 17 '21 at 10:45
  • 5
    `register` was deprecated in c++11 – Jonathan Potter Mar 17 '21 at 10:46
  • @largest_prime_is_463035818, okay but in your example its using std:vector. For the sake of understanding how the memory allocation for variables works, how would it be done with a naked array? – Alasdair Mar 17 '21 at 10:48
  • for a naked array you would use a `std::unique_ptr`, the thing is: You don't need completely naked dynamic arrays (unless for homework exercises and for very special rare cases) – 463035818_is_not_a_number Mar 17 '21 at 10:50
  • But isn't std library just allocating it on the heap, same as `new`? This is the point I'm getting at. I understand its not recommended to use C-style arrays, but I'm failing to see what you are saying is the alternative to `new`. – Alasdair Mar 17 '21 at 10:53
  • I've always considered that global variables were stored on the stack, just at a lower level than the main() itself, so I'm not sure they need their own category. Not a compiler expert though. – m88 Mar 17 '21 at 10:56
  • 1
    @Alasdair The whole point is to take the burden of managing it properly from you. `std::vector` allocates, copies and destroys objects correctly, with exception safety and every other safety you can think of. When using bare `new`, you have to do that work yourself, and it is may be hard to keep it actually safe and without leaks. – Yksisarvinen Mar 17 '21 at 10:56
  • I'm asking a question about memory space for understanding what's going on, not about best practice. The `std::vector` is allocating the memory in the same memory space that `new` is allocating it, i.e. the heap, or its not? This is important because it changes how I use it. – Alasdair Mar 17 '21 at 10:58
  • 3
    _"Any variables created inside a function (non-global) that is not static or thread_local and is not created with new, will be on the stack."_ — That's a very common **myth**. I would rather suggest considering variables being mapped somewhere than to be stored there (it's an abstraction; CPUs don't know the concept of a variable). Only what matters is the _as-if rule_. With enabled optimizations, variables may be mapped to the stack as well as to registers as well as to nowhere (may be optimized away). Note that these are the implementation issues, the C++ standard does not care. – Daniel Langr Mar 17 '21 at 11:04
  • @m88 No. They aren't. They, like the space taken up by your program's code, are allocated by the system linker when it starts your program. They won't be on the stack; they might even, depending on the system and compiler, be intermingled with the program code itself. – alastair Mar 17 '21 at 11:05
  • @Alasdair Yes, `std::vector` will allocate from the heap. – alastair Mar 17 '21 at 11:08
  • @DanielLangr I don't think it's a myth so much as people not being 100% precise. But it's a good point. – alastair Mar 17 '21 at 11:10
  • 2
    @alastair to be completely correct, it would allocate where allocator would. Default allocator would do that in heap, but you can specify your own. Because it's not integral part of vector (and most similar dynamic containers in standard library) – Swift - Friday Pie Mar 17 '21 at 11:12
  • @Swift-FridayPie Sure, though custom allocators are, sadly, trickier than they look. – alastair Mar 17 '21 at 11:15
  • 2
    @Swift-FridayPie To be super-completely correct, the C++ standard doesn't know the concept of the heap. I don't know about any heapless C++ implementations, but still, heap is the implementation matter. – Daniel Langr Mar 17 '21 at 11:17
  • sorry for taking the discussion elsewhere. My comment about dynamic allocation vs using `new` directly was just a tangent. Under the hood there is a `new` also in `std::vector`. I just wanted to clarify that "Heap variables are allocated with the new keyword, which returns a pointer to the memory address on the heap." misses the point of how one should manage dynamic allocated objects – 463035818_is_not_a_number Mar 17 '21 at 11:17
  • see eg here for a good example to motivate `make_shared` instead of `new`: https://stackoverflow.com/a/18242554/4117728. Again, its not about not using `new` at all, its about not using `new` in your code – 463035818_is_not_a_number Mar 17 '21 at 11:19
  • 2
    @largest_prime_is_463035818 `std::vector` doesn't use `new T` (or even `new T[]`) as it has to separate allocation from construction. It could use placement `new` – Caleth Mar 17 '21 at 11:27
  • 1
    @Caleth yeah I realized just a moment ago my comments werent quite right. Actually "Heap variables are allocated with the new keyword ..." is more wrong than I was implying. `new` is one way to dynamically allocate memory, but dynamic allocation does not require to use `new`. Now I feel sorry for the confusion I caused :/ – 463035818_is_not_a_number Mar 17 '21 at 11:30
  • @alastair I know they technically aren't (data/code segment and all that) but from a user perspective, global variables behave as-if they were stack-allocated variables in one global parent scope. – m88 Mar 17 '21 at 11:32
  • @m88 except that they can be forward declared, and the initialisation order is implementation-defined, so "one massive parent scope of `main`" is wildly different to ordinary scope – Caleth Mar 17 '21 at 11:37
  • @alastair it's mostly to avoid terminology confusion. A "heap" is a term from set theory (and we have a function `make_heap`). I really prefer to avoid it usually, but 30 year old habit dies hard , especially while some reprints of old books still use the term . Even in existing OSes memory managed better than a heap (usually). How dynamical allocation happens is understandably delegated to implementation. – Swift - Friday Pie Mar 18 '21 at 08:43

3 Answers3

1

Mostly right.

Any variable that is accessed with a pointer is stored on the heap.

This isn't true. You can have pointers to stack-based or global variables.

Also it's worth pointing out that global variables are generally unified by the linker (i.e. if two modules have "int i" at global scope, you'll only have one global variable called "i"). Dynamic libraries complicate that slightly; on Windows, DLLs don't have that behaviour (i.e. an "int i" in a Windows DLL will not be the same "int i" as in another DLL in the same process, or as the main executable), while most other platforms dynamic libraries do. There are some additional complications on Darwin (iOS/macOS) which has a hierarchical namespace for symbols; as long as you're linking with the flat_namespace option, what I just said will hold.

Additionally, it's worth talking about initialisation behaviour; global variables are initialised automatically by the runtime (typically either using special linker features or by means of a call that is inserted into the code for your main function). The order of initialisation of globals isn't guaranteed. However, static variables declared at function scope are initialised when that function is first executed, and not at program start-up as you might suppose, and that feature is commonly used by C++ programmers to do lazy initialisation.

(Similar concerns apply to destructors for global objects; those are best avoided entirely IMO, not least because on some platforms there are fast termination features that simply won't call them.)

const keyword means you can't change the variable.

Almost. const affects the type, and there is a difference depending on where you write it exactly. For example

const char *foo;

should be read as foo is a pointer to a const char, i.e. foo itself is not const, but the thing it points at is. Contrast with

char * const foo;

which says that foo is a const pointer to char.

Finally, you've missed out volatile, the point of which is to tell the compiler not to make assumptions about the thing to which it applies (e.g. it can't assume that it's safe to cache a volatile value in a register, or to optimise away accesses, or in general to optimise across any operation that affects a volatile value). Hopefully you'll never need to use volatile; it's most often useful if you're doing really low-level things that frankly a lot of people have no need to go anywhere near.

alastair
  • 4,199
  • 20
  • 30
  • Everything I do is really low level... that's why I'm trying to figure all this out. So could you give me an example of where volatile would be used? – Alasdair Mar 17 '21 at 11:18
  • `volatile` is for when something *outside* your process changes an object. Typically this is some hardware that is mapped into some range of addresses. – Caleth Mar 17 '21 at 11:22
  • It's also sometimes used when doing low level things in multithreaded code without using locking (i.e. the other piece of hardware might be another CPU core). Doing that kind of thing is fraught with peril, mind; you need a good understanding of the underlying hardware (particularly wrt things like memory ordering), and you're very likely to end up doing things that from a C/C++ standpoint constitute undefined behaviour. – alastair Mar 17 '21 at 11:55
  • OK, I think I get it. From your description it looks like in most cases when I take a pointer I would actually want to use `T * const foo`, assuming I'm not intending to recycle the pointer variable. Is that right? – Alasdair Mar 17 '21 at 12:27
  • 1
    @Alasdair Yes, if you don't want to change what it points at, that's right. – alastair Mar 17 '21 at 13:51
  • @alastair No, don't use `volatile` for threading. That's very wrong. We have atomics since C++11. BTW, for Caleth, `volatile` is not only for when _something outside of the process changes its object_. It's also for when _something outside of the process needs to observe the changes of its object_. – Daniel Langr Mar 19 '21 at 10:42
  • 1
    @DanielLangr I entirely agree that users should generally be using atomics or higher-level primitives like locks, semaphores and so on. That's why I said using `volatile` was "fraught with peril", and I said it was *sometimes used*, not that any given person *should* use it. (It's also not "very wrong"; there are situations where `volatile` would be sufficient and where you might not want the overhead of the special bus transactions for atomics. If you don't know if you're in that situation, however, you aren't in it.) – alastair Mar 19 '21 at 11:51
1

The other answer is correct, but doesn't mention the use of register.

The compiler will probaby ignore this and apply it to whatever it thinks would be the best improvement.

This is correct. Compilers are so good at choosing variables to put in registers (and typical programmer is bad at that), that C++ committees decided it's completely useless.

This keyword was deprecated in C++11 and removed in C++17 (but it's still reserved for possible future use).

Do not use it.

Yksisarvinen
  • 13,037
  • 1
  • 18
  • 42
  • :-D I'm not so sure compilers *are* good at choosing variables to put in registers; I'm pretty sure humans writing in assembly language will often do a better job. But yes, since it's deprecated in C++11 and will be removed in C++17, it's best avoided. – alastair Mar 17 '21 at 12:01
  • Yeah it occurs to me that there should really be the ability to temporarily assign a `register` variable for a very limited defined scope. I don't really agree with the industry movement towards limiting the programmers' ability to directly manage the memory. It's created a greater division between low-level and high-level coding, where you'd now have to code assembly to do something that you could previously do in C or C++. If I wanted the language to do everything for me, I'd use Rust or Go. – Alasdair Mar 17 '21 at 12:50
1

You need to differentiate between specification and implementation. The specification does not say anything about stack and heap, because that's an implementation detail. They purposely talk about Storage duration.

How this storage duration is achieved depends on the target environment and if the compiler needs to do allocations those at all or if these values can be determined at the compile-time, and are then only part of the machine code (which for sure is also at some part of the memory).

So most of your descriptions would be For the target platform XY it will generally allocate on stack/heap if I do XY

C++ could also be used as an interpreted language e.g. cling that could have completely different ways of handling memory.

It could be cross-compiled to some kind of byte interpreter in which every type is dynamically allocated.

And when it comes to embedded systems the way how memory is managed/handled might be even more different.

Heap variables are allocated with the new keyword, which returns a pointer to the memory address on the heap.

If the default operator new, operator new[] are mapped to something like malloc (or any other equivalent in the given OS) this is likely the case (if the object really needs to be allocated).

But for embedded systems, it might be the case that operator new, operator new[] aren't implemented at all. The "OS" just might provide you a chunk of memory for the application that is handled like stack memory for which you manually reserve a certain amount of memory, and you implement a operator new and operator new[] that works with this preallocated memory, so in such a case you only have stack memory.

Besides that, you can create a custom operator new for certain classes that allocates the memory on some hardware that is different to the "regular" memory provided by the OS.

The std::vector is allocating the memory in the same memory space that new is allocating it, i.e. the heap, or its not? This is important because it changes how I use it.

A std::vector is defined as template<class T, class Allocator = std::allocator<T>> class vector; so there is a default behavior (that is given by the implementation) where the vector allocates memory, for common Desktop OS it uses something like OS call like malloc to dynamically allocate memory. But you could also provide a custom allocator that uses memory at any other addressable memory location (e.g. stack).

t.niese
  • 32,069
  • 7
  • 56
  • 86
  • One of my primary concerns is not overflowing the stack. Often I'm working with very large arrays, the size for which is known. Let's say I have no particular need for a pointer. My thinking was that I should use `new` (or a fancy, safe alternative) for this because it will give me more memory space, and if I just allocate it directly it'll overflow the stack. But you seem to be saying that these are not directly connected concepts. In which case, what would be the most efficient way of allocating a very large structure with a known size? – Alasdair Mar 17 '21 at 14:19
  • @Alasdair for a given architecture it can be said so if you says you develop any of the combinations of macOS/Linux/Windows on x86/x64/ARM then memory allocation (stack/heap) is really similar, and is mostly close to what you have said. But if it is only about c++ in general then this cannot be answered. – t.niese Mar 17 '21 at 14:29
  • @Alasdair `Often I'm working with very large arrays, the size for which is known.` then either use `std::vector` with a `reserve(known_size)` or `std::unique_ptr>` (which is how you say allocated on the heap for most OS). Heap allocations are mostly considered as bad compared to stack, not because memory access in heap is slow (it is the same memory), but if you request memory on the heap then you ask the OS to reserve some memory which is expensive, while memory allocation on the stack can be done without the os. – t.niese Mar 17 '21 at 14:29
  • Makes sense. The efficiency concern was more about the extra time it takes to lookup the memory address from the pointer (which is itself a variable on the stack). It's a few more operations than without a pointer, and most of the time I'm making quazillions of tiny changes very quickly all over the array and this is the main bottleneck. Anything to take an operation out of that is worthwhile for me. I'll try to use `static` instead if I can then, I guess that's better for that and won't be on the stack (at least not subject to overflow)? – Alasdair Mar 17 '21 at 14:37
  • @Alasdair that's a completely different question but has the same problem as your current question. Your assumption is that the generated code does everything exactly as the standard states, or as it is given by the code of the implementation. This is true if you compile without optimizations. So without optimizations, a `std::vector` might be slower than an `std::array` which might be slower than a c-style array. But with optimizations on most overhead regarding that is optimized away. To work with them you always need a pointer to the memory location (which is either on stack or heap). – t.niese Mar 17 '21 at 15:18
  • @Alasdair Optimized code only needs to follow the as-if rule. And with optimizations turned on retrieving these memory locations and accessing the objects takes the same amount of instructions for each of them. So if you don't perform any allocations when you process the data, and you don't use `.at()`, but `operator[]` for `std::vector` then differences in performance more likely result from cache misses. – t.niese Mar 17 '21 at 15:18
  • Yeah okay. In my experience (with other languages) when someone says "the compiler will optimize it anyway and it'll be the same" - it's not. It'll come in and out of being the same depending on the version of the compiler and various unknown factors. Sometimes it is, and sometimes it isn't. Change some code, and it's optimized differently. I try to code as efficiently as possible anyway. And at the end of the day I understand the code better when I can see what its doing. – Alasdair Mar 17 '21 at 15:27
  • @Alasdair that is in general true. But even if the compiler would not be able to optimize the overhead of the member function calls aways, then the performance difference between `std::array` and `std::vector` for lookups should still be the same. For a very bad compiler a c-style array might be faster, but then you have different problems. – t.niese Mar 17 '21 at 15:35
  • @Alasdair The situation in C++ is not really any different. Compilers are reasonably good at optimising away the STL in straightforward situations, especially if you write "const" in all the right places, but outside of that YMMV. Godbolt (https://godbolt.org) is very helpful as a way to explore the code that might be generated in any given case. – alastair Mar 18 '21 at 11:32
  • @Alasdair If you're really trying hard to optimise things, one thing to remember with things like `std::vector` is that the implementations are generic and hence fairly naïve — though you can sometimes help to give the implementation a hint (e.g. if you know you're going to insert 10,000 elements, you can reserve the capacity for it up front to improve performance). If your arrays get very large indeed, or you're routinely inserting in the middle or some such, it might be worth contemplating other options (e.g. on macOS, CFArray - see https://ridiculousfish.com/blog/posts/array.html). – alastair Mar 18 '21 at 11:41
  • @alastair tbh I have no intention of using std library. I see *all* data types as abstractions over blocks of memory split into 8 bytes of 8 bits. Just using a C array is already an abstraction as far as I'm concerned. – Alasdair Mar 18 '21 at 13:10