Search code examples
c++operator-overloadingnew-operator

Running non-default constructor from class-overloaded new operator


I want a class Watchdog to create its objects in POSIX shared memory.

Keep in mind source code for this class is compiled into a static library and the source code for the methods are actually placed in a c++ source file.

My first attempt at this was having a static Watchdog* createWatchdog(param p1, param p2, ...) function in my class, and use this as a wrapper for allocating the memory of the object, then calling a placement new (ptr) Watchdog.

This worked fine but then I remembered the new operator could be overloaded so I came up with this solution:

Watchdog.h

#pragma once

#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

namespace og {

class Watchdog {
public:
    Watchdog();
    Watchdog(const std::string t_name, void* t_data, const size_t t_datSize);

    static void* operator new(size_t count, const std::string t_name, const size_t t_datSize);


protected:
    Watchdog(const Watchdog&) = delete;
    Watchdog& operator=(const Watchdog&) = delete;

private:
    std::string m_name;
    void* m_data;
    size_t m_datSize;

    // and other member variables
}; // end class Watchdog

} // end namespace

Watchdog.cxx

#include "Watchdog.h"

using namespace og;

Watchdog::Watchdog()
{}

Watchdog::Watchdog(const std::string t, void* t_data, const size_t size)
{}

void* operator new(size_t count, const std::string t_name, const size_t dat)
{
     int fd;
     void* obj;

     // removed all error checks for the sake of the example
     fd = shm_open(t_name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600);
     (void) ftruncate(fd, sizeof(Watchdog));
     obj = mmap(0x0, sizeof(Watchdog), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
     (void) shm_unlink(t_name.c_str());

     return obj;
 }

CMakeLists.txt

CMAKE_MINIMUM_REQUIRED( VERSION 2.6 )
PROJECT( WATCHDOG C CXX )

SET( CMAKE_CXX_STANDARD 11)
SET( CMAKE_CXX_STANDARD_REQUIRED OFF)

SET( watchdog_HEADERS
 Watchdog.h
)

SET( watchdog_SRC
 Watchdog.cxx
 ${watchdog_HEADERS}
)

SET( CMAKE_CXX_FLAGS_DEBUG "-g3 -Wall -Wextra -Wpedantic")

ADD_LIBRARY( watchdog STATIC ${watchdog_SRC} )
TARGET_LINK_LIBRARIES( watchdog -lrt )

Compiled with cmake . && make

main.cxx

#include "Watchdog.h"

using namespace og;

int
main()
{
    Watchdog* W;

    W = new ("test", 20) Watchdog;
}

  1. Compiling with g++ main.cxx -L. -lwatchdog -lrt raises that linkage error:

/usr/lib64/gcc/x86_64-suse-linux/9/../../../../x86_64-suse-linux/bin/ld: /tmp/ccDncNvb.o: in function `main': main.cxx:(.text+0x3c): undefined reference to `og::Watchdog::operator new(unsigned long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, unsigned long)' collect2: error: ld returned 1 exit status


  1. Do I have to initialize all member variables by hand in the operator new method? I know I can do return ::operator new (sz) or return ::operator new(sz, ptr) from the operator new method but I fail to see how I can call instead a non-default constructor (such as the second one in the class specification).

Solution

  • Question 1:

    Your definition

    void* operator new(size_t count, const std::string t_name, const size_t dat) //...
    

    in Watchdog.cxx defines a free user-defined operator new, not the class-specific overload, that you declared in the class Watchdog.

    Instead use:

    void* Watchdog::operator new(size_t count, const std::string t_name, const size_t dat) //...
    

    Since the class-specific overload is chosen for the new-expression in main, its definition will be missing.


    Question 2:

    operator new only returns a pointer to raw memory. It is not responsible for constructing objects in it.

    Objects are constructed by the new-expression with a call to a constructor as necessary. The syntax follows that of variable definitions. For example

    W = new ("test", 20) Watchdog;
    

    calls the default constructor for the Watchdog object, while

    W = new ("test", 20) Watchdog(arg1, arg2, arg3);
    

    will try to call a constructor overload matching the three arguments.

    The first parameter list in the new-expression is not used as arguments to the constructor, but as arguments that the allocation function needs in order to provide the correct pointer to memory in which the object can be constructed.


    Also note, that the operator new should allocate memory for (at least) count (first argument) bytes. This argument will be provided with the correct value required to construct the object by the new-expression. This is in particular important when you are using the array versions of operator new and new.


    Also be aware that you are constructing only the Watchdog object itself in the shared memory provided by your operator new overload. Should your class use new to allocate memory for e.g. the void* m_data member, it will not use your operator new for the allocation, but the regular allocation function, which will allocate (as usual) in the processes (unshared) memory space.

    The same applies to memory allocated by non-trivial members of your class, such as std::string m_name. When it is going to need to allocate memory (if SSO is insufficient), it will allocate the memory for the string data with the regular allocation function (in unshared memory), not your operator new overload.

    This means, for example, that m_name cannot be safely used from the other process you are sharing memory with.