1

In order to track our memory usage and display some runtime statistics to the user (in a performant way) I'm overriding global new/delete (with a #define in a header - adding that header on top of every source file where we need to allocate/deallocate memory is going to ensure we can track all allocations going on).

I have two questions at this point since it's a multi-platform C++ codebase:

  • this is old code and some places still use malloc next to new. I think I also need to override global malloc/free/calloc/realloc, is that correct?
  • STL containers: we use them a lot (e.g. std::vector). If I include my re-#defining header at the top of every source file, do I still need to pass a custom allocator like std::vector<int, my_allocator<int>>? Or are the globally re-defined new/delete enough? I think I still need the custom allocator but I'm not sure.
Dean
  • 6,610
  • 6
  • 40
  • 90
  • The `std::vector` implementation does not use your versions of `new/delete` so yes... you need to provide the allocator. – Fareanor Nov 02 '22 at 12:59
  • 1
    Do not redefine `new`/`delete` via `#define`. This is a really bad idea, and also completely unnecessary. Redefine them *properly*, then your second question resolves itself. Incidentally, the same is true should you opt to redefine `malloc` and `free`. – Konrad Rudolph Nov 02 '22 at 13:10
  • Many years ago Microsoft set a really bad pattern when they used macros that redefined `new` and `delete` to create debugging versions. Don't follow that example. A program that uses macros to redefine any C++ keyword has undefined behavior. You can write your own `operator new` and `operator delete` and use them instead of the library-supplied versions, and all of your code (including standard library containers) will use your versions. – Pete Becker Nov 02 '22 at 13:41
  • @KonradRudolph by 'proper' you mean providing custom allocators to std containers and rewriting all calls to global new/delete to use my custom ones? – Dean Nov 02 '22 at 13:58
  • 1
    @Dean No. I mean redefining `operator new` and `operator delete` in the global namespace by using their poper function definitions. Armin’s answer gives an example of that. – Konrad Rudolph Nov 02 '22 at 13:59
  • @KonradRudolph thanks, maybe you know the answer to this related issue to your approach: how come redefining the new operator globally as you suggested won't give out linking errors? Is std::vector header-only (and so are all the STL containers) so that they link against whatever operator new is defined in the translation unit? I'm getting very confused as to where the STL operator new is defined and why overriding it won't provoke symbol clashing – Dean Nov 02 '22 at 14:51
  • @Dean It won't give a linker error because C++ explicitly allows this redefinition. How this is solved technically isn't specified by the standard, and you're right that you couldn't just do this with other functions without causing an ODR violation. – Konrad Rudolph Nov 02 '22 at 15:05
  • @Dean … and I should have mentioned that C++ does *not* allow redefining `malloc` and `free` in the same way. To do this properly you'll have to use implementation-defined solutions. For example, for implementations using libc you could use [`malloc` hooks](https://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html), or a preload library (via `LD_PRELOAD`). – Konrad Rudolph Nov 02 '22 at 15:30
  • @KonradRudolph thanks Konrad! One last pickle: my initial thought was to override `operator new`, count the number of bytes allocated (or deallocated), then call the _real_ `operator new`. That would have also made sure that every STL container used whatever we needed, but it seems I can't do that anymore (I can't call `::operator new` from my custom `operator new` override - it will recursively call itself). Any way to solve this? Alternatively, I will have to put my overrides in a class and also provide an allocator in all of my codebase STL containers.. – Dean Nov 02 '22 at 15:50
  • Agh crap: https://stackoverflow.com/a/4134355/3834459 – Dean Nov 02 '22 at 15:57

2 Answers2

3

First question: Yes

  • The details of how operator new is implemented are property of a particular implementation of standard library - not even a compiler or operation system. All of them, at least, the three big ones, (CLang, GCC and MSVC) are using malloc() within operator new, because it just makes a life of library developer so much easier. So, yes, you will need to override the malloc/free/calloc/realloc.

Second question: No

  • std::vector uses std::allocator by default, and std::allocator is required to use global operator new, that is, ::operator new(size_t) to obtain the memory. However, it isn't required to call it exactly once per call to allocator::allocate.

  • If you replace global operator new, then vector will use it, although not necessarily in a way that really allows your implementation to manage memory "efficiently". Any special tricks you want to use could, in principle, be made completely irrelevant by std::allocator grabbing memory in 10MB chunks and sub-allocating.

You are fighting against a legacy old codebase. Any pure text replacement, like #define preprocessor directives are really discouraged (luckily, some day C++ will get rid out of preprocessor), but, in your concrete case, it could be a viable approach.

Alex Vergara
  • 1,766
  • 1
  • 10
  • 29
1

If containers, like for example std::vector use the default allocater std::allocater as described here, then the std::allocaterwill call the global new function in its allocatefunction, as described here.

Allocates n * sizeof(T) bytes of uninitialized storage by calling ::operator new(std::size_t) or ::operator new(std::size_t, std::align_val_t) (since C++17)

So, it will call the global new and if you override it then your implementation will be called. Please see the below example:

#include <iostream>
#include <iostream>
#include <vector>
#include <stdlib.h>
 
using namespace std;
void * operator new(size_t size)
{
    cout << "New operator overloading " << endl;
    void * p = malloc(size);
    return p;
}

int main()
{
    std::vector<int> v{};
    v.resize(50);
    v.resize(1500);
}
A M
  • 14,694
  • 5
  • 19
  • 44
  • 1
    One minor caution: there is nothing in the standard that prohibits stream inserters from calling `operator new`, and if that happens, this code could recurse infinitely. In practice this isn't a problem, but I always use `printf` for debug output from memory managers, just in case... – Pete Becker Nov 02 '22 at 13:45
  • 1
    One major caution: the code in this answer doesn't deal with `malloc` returning a null pointer, which it will do if it cannot allocate memory. Plain old `operator new` is not allowed to return a null pointer. Any replacement for `operator new` should check for a null pointer and throw an exception when it sees one. – Pete Becker Nov 02 '22 at 13:46
  • Of course you and I wouldn't. But innocent readers might... – Pete Becker Nov 02 '22 at 13:56
  • @ArminMontigny thanks, one minor point I'm still confused on: how come redefining the new operator globally isn't giving out linking errors? Is std::vector header-only (and so are all the STL containers) so that they link against whatever operator new is defined in the translation unit? I'm getting very confused as to where the STL operator new is defined and why this is overriding it without provoking symbol clashing.. – Dean Nov 02 '22 at 14:38