2

I've got a tiny little utility class called ObjectCounter that has no virtual methods and no member-variables; all it contains is a constructor and a destructor, which increment and decrement a global variable, respectively:

int _objectCount = 0;  // global

class ObjectCounter
{
public:
   ObjectCounter() {printf("DefaultCtor:  count=%i\n", ++_objectCount);}
   ~ObjectCounter() {printf("Dtor:  count=%i\n", --_objectCount);}
};

When I want to track the number of instances of another class that my program has created at any given time, I just add an ObjectCounter as a private-member-variable to that class:

class SomeUserClass
{
public:
   SomeUserClass() : _userData(0) {/* empty */}
   ~SomeUserClass() {/* empty */}

private:
   ObjectCounter _objectCounter;
   int64_t _userData;
};

(originally I would have that class subclass ObjectCounter instead, but doing it that way was making my DOxygen class-graphs unnecessarily complex, so I changed to making it into a private member variable)

Today I noticed that adding this "empty" private member variable to my class objects often increases the sizeof the class objects (as reported by sizeof()). For example, the following code shows that sizeof(SomeUserClass) increases from 8 to 16 on my machine when I include the _objectCounter member-variable:

int main(int, char **)
{
   SomeUserClass sc1;
   printf("sizeof(SomeUserClass)=%zu\n", sizeof(SomeUserClass));
   printf("sizeof(ObjectCounter)=%zu\n", sizeof(ObjectCounter));

   return 0;
}

The increase happens with or without optimizations enabled (via -O3).

I believe the reason for that is that the compiler is allocating space for the _objectCounter member-variable simply so that if some other code needs to get a pointer-to-ObjectCounter, a unique address can be supplied. But no code in my program ever actually does reference the _objectCounter variable; it exists solely to execute its own default-constructor and destructor at the appropriate times.

Given that, is there any way to encourage (or better yet, force) the compiler to not allocate any space for this member-variable?

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • 1
    Try making the `int64_t` class member the first member of the class. – Sam Varshavchik Jun 07 '21 at 22:20
  • @SamVarshavchik I tried it, `sizeof(SomeUserClass)` is still 16. – Jeremy Friesner Jun 07 '21 at 22:22
  • 2
    Inherit from `ObjectCounter` so that empty base optimization can apply. In most cases it will. – Sopel Jun 07 '21 at 22:22
  • Tangential: It'd be simple enough to change `_objectCount` from a global variable to a static class variable belonging to `ObjectCounter`. Simple but requires more typing is changing it to respect the rule of 3/5/0 by adding copy and move constructors that also increment the counter as appropriate. – Nathan Pierson Jun 07 '21 at 22:22
  • @Sopel that does work but see my third paragraph for why I'm trying to avoid doing that. – Jeremy Friesner Jun 07 '21 at 22:23
  • 3
    If you're using C++20 then there's attribute `[[no_unique_address]]` – Sopel Jun 07 '21 at 22:24
  • side note, because `int _objectCount = 0; // global` is global, [the identifier is illegal](https://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier). – user4581301 Jun 07 '21 at 22:24
  • I'm not too familiar with doxygen, but did you do private inheritence? Some folks had trouble *exposing* documentation from private bases. https://stackoverflow.com/q/36421785/817643 – StoryTeller - Unslander Monica Jun 07 '21 at 22:31
  • @StoryTeller-UnslanderMonica yes, private inheritance it was. – Jeremy Friesner Jun 07 '21 at 22:35
  • Does `#if __has_cpp_attribute(no_unique_address)` ... `#endif` say the C++20 feature is available? As per: https://en.cppreference.com/w/cpp/feature_test – Eljay Jun 07 '21 at 22:48

1 Answers1

4

If you can use C++20, you can use the attribute [[no_unique_address]] to accomplish this. Using

#include <cstdio>
#include <cstdint>

int _objectCount = 0;  // global

class ObjectCounter
{
public:
   ObjectCounter() {printf("DefaultCtor:  count=%i\n", ++_objectCount);}
   ~ObjectCounter() {printf("Dtor:  count=%i\n", --_objectCount);}
};

class SomeUserClass
{
public:
   SomeUserClass() : _userData(0) {/* empty */}
   ~SomeUserClass() {/* empty */}

private:
   [[no_unique_address]] ObjectCounter _objectCounter;
   int64_t _userData;
};

int main(int, char **)
{
   SomeUserClass sc1;
   printf("sizeof(SomeUserClass)=%zu\n", sizeof(SomeUserClass));
   printf("sizeof(ObjectCounter)=%zu\n", sizeof(ObjectCounter));

   return 0;
}

Outputs:

DefaultCtor:  count=1
sizeof(SomeUserClass)=8
sizeof(ObjectCounter)=1
Dtor:  count=0
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • This seems like a good solution, except that when I try it (with `g++ -O3 temp.cpp -std=c++20`, using Apple clang v12.0.5 from XCode 12.5), `sizeof(SomeUserClass)` still gets printed as 16 even with the `[[no_unique_address]]` tag applied. Compiler bug, perhaps? – Jeremy Friesner Jun 07 '21 at 22:34
  • @JeremyFriesner Bug maybe, could not be implemented yet. IIRC g++ on MacOS is actually clang, and at least clang 12.0 gives the correct result: https://godbolt.org/z/W5oaGG8vd – NathanOliver Jun 07 '21 at 22:49
  • Ah, I see the difference -- if I declare `_userData` first, I get sizeof=16, but if I declare `_objectCounter` first, I get sizeof=8. – Jeremy Friesner Jun 07 '21 at 23:04