2

I'd like an accounting/instrumentation layer which is supposed to count a number of different "events" on instances of objects if so desired.
An "event" can be anything that happens. For example, at the end of the day, you should be able to tell "file was written to 5400 times", or "lock was congested 52,456 times, uncongested 12,311 times, acquired 33,470 times by spinning, wait timed out 609 times". Whatever. Anything you like.

So, if something happens, you write ++some_counter and that's it. Easy enough.

The catch is that when you turn the feature off, it shouldn't add 24 or 36 or more bytes to every instance of some class since there may be quite a few instances!

Basically, I'm thinking (simplified, not threadsafe) along something like this:

#include <type_traits>

struct count // actually counts stuff
{
    int val = 0;
    count& operator++() { ++val; return *this; }
    operator int() { return val; }
};

struct null_count // just the interface
{
    null_count& operator++() { return *this; }
    operator int() { return 0; }
};

template<bool enable> struct someclass
{
    using counter_t = std::conditional_t<enable, count, null_count>;

    counter_t a;
    counter_t b;
    counter_t c;
    counter_t d;
    counter_t e;
    counter_t f;    // might have 10 or 20 of them?!

    void blah()  { ++a; }
    void blubb() { ++b; }
    void foo()   { ++c; }
    void bar()   { ++d; } // whatever
};

The size of someclass<true> will, unsurprisingly, be 24 in this example, since it has to store those integers somewhere. That's OK because after all I want these. I'm interested in those numbers.

Now, ideally the size of someclass<false> should be zero (or rather, 1 since that is the minimum the standard will allow us to have). It is, however, 6 since each of the counter_ts must, of course, have a size of at least 1 as well (even if there's truly nothing in there).
If a class originally consists of, say, nothing but a handle, and you more than double (or maybe triple?) its size just because of a lot of empty objects which do not do anything useful, and you maybe have a few hundred or thousand instances, well... then that stinks. This is not something you'd want.

There exists [[no_unique_address]] as of C++20, which seems to do just what I want. Unluckily, compiler support to date is, uh... disappointing. My GCC doesn't like it. I haven't tested it either, for that very reason.

There exists a feature "empty base class optimization" which has existed and supported since pretty much forever. That sounds like it might solve the problem.
The issue is that I can move the counters into a base class, sure. I can also conditionally derive from base or from empty, no problem.

But then, all code incrementing a counter would be invalid (you can hardly reference a member that doesn't exist at all!). I would thus have to wrap every increment in an #ifdef or the like which is very undesirable. Or use a macro that does more or less the same thing (much alike assert strips out code in release). Which, although it would "work" is just as undesirable.
Naively, one might be inclined to say: "Hey, you can use if constexpr" which would be mighty fine, except, unluckily, that won't compile.

Is there another straightforward way of optimizing out "empty" data?

JJJ
  • 32,902
  • 20
  • 89
  • 102
Damon
  • 67,688
  • 20
  • 135
  • 185
  • 1
    if I understand correctly, you simply want a `template <> struct someclass {};` specialization. If not, I have no clue what you want ;) – 463035818_is_not_an_ai Aug 12 '19 at 11:47
  • tbh I dont really see what `someclass` and `someclass` have in common, maybe you just need `someclassWithCounter` and `someClassWithoutCounter`, no templates needed here. Why do you want to make them share some common implementation? (`someclass` and `someclass` are two completely unrelated types anyhow) – 463035818_is_not_an_ai Aug 12 '19 at 11:50
  • @formerlyknownas_463035818 Ah, I believe that the op still wants `++a` `++b` and all such stuff to compile but with no-op. – ph3rin Aug 12 '19 at 11:54
  • @KaenbyouRin then still I dont see a reason for all the complexity. In that case the two classes `someclassWithCounter` and `someclassWihoutCounter` should share a common interface and `someclassWithoutCounter` does nothing in those methods – 463035818_is_not_an_ai Aug 12 '19 at 11:55
  • 1
    How about this? [**Demo on coliru**](http://coliru.stacked-crooked.com/a/d88a59cc506ce23e). At least, I could reduce the footprint for disabled counters significantly... ;-) – Scheff's Cat Aug 12 '19 at 12:11
  • @Scheff I believe your solution might be elaborated to employ EBO. Some exemplary attempt: http://coliru.stacked-crooked.com/a/ab59333b5675282f. – Daniel Langr Aug 12 '19 at 12:41
  • 1
    @DanielLangr That looks promising. I would enjoy your answer. (I must admit that my template programming abilities are rather weak. So, I'm not surprised that you found a way to improve my attempt.) – Scheff's Cat Aug 12 '19 at 13:16
  • There seems to be confusion about there being a template. The template is just there to _somehow_ pass a config value (turn stuff on/off). It could as well have been a constexpr global variable instead. – Damon Aug 12 '19 at 17:16

2 Answers2

3

Solution

Apply [[no_unique_address]] attribute to empty data members.

Note

  • [[no_unique_address]] attribute is fully supported by GCC.
  • If your version of GCC doesn't support it properly, update it.
  • If you have newest version of GCC, either report defect or upvote it.
  • Wait for GCC patch that will make use of [[no_unique_address]] properly.

Summary

Use [[no_unique_address]]. It should start working soon.

2

The problem in your solution is that you have 6 independent member variables each of them occupying at least of a single byte, as you are aware of. A better way might be to store the counters into an array, as @scheff suggested in the comments section.

template <size_t N>
class counters {
   std::array<size_t, N> values{};

public:    
   template <size_t I>
   void inc() {
      static_assert(N > I);
      values[I]++;
   }

   template <size_t I>
   size_t get() const {
      static_assert(N > I);
      return values[I];
   }
};

To provide a uniform interface — valid incrementing code in case of disabled counters — a template specialization might be used. In this specialization, there will be no array and empty function bodies:

template<>
class counters<0> {
public:
   template <size_t I> void inc() { }
   template <size_t I> size_t get() const { return 0; }
};

Finally, the counters might be conditionally injected into your classes be either composition of inheritance. I recommed the latter case, since in case of disabled counters, empty base optimization will be applied:

template<bool enable, size_t N = enable ? 4 : 0>
class potentially_counted : private counters<N> {
   int handle;  // class' data

   enum { BLAH, BLUBB, FOO, BAR };

public:
   void blah()  { counters<N>::template inc<BLAH>(); }
   void blubb() { counters<N>::template inc<BLUBB>(); }
   void foo()   { counters<N>::template inc<FOO>(); }
   void bar()   { counters<N>::template inc<BAR>(); }
};

A simple test code:

int main()
{
  std::cout << "sizeof(potentially_counted<true>): "
     << sizeof(potentially_counted<true>) << std::endl;
  std::cout << "sizeof(potentially_counted<false>): "
     << sizeof(potentially_counted<false>) << std::endl;
}

will likely show that with disabled counting, size of the class will be the same as size of int. Live demo: https://wandbox.org/permlink/sMRrnhZvevozlCBL.


UPDATE

To avoid that non-pleasant templated call, you can wrap it into a separate member function:

template <size_t I>
void inc_cnt() { counters<N>::template inc<I>(); }

And use it as follows:

void blah()  { inc_cnt<BLAH>(); }
Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • 1
    I had to lookup "EBO" (I didn't know this term) and found [SO: When do programmers use Empty Base Optimization (EBO)](https://stackoverflow.com/a/4325624/7478597) which adds nicely to your answer. – Scheff's Cat Aug 12 '19 at 14:09
  • This is pretty good. The invocation is a tidbit obnoxious to look at (shame there's no easy way of `using` all them templates in the base, at least none that I'd remember), but I really like the solution as such. Does exactly what I wanted, and no macros. – Damon Aug 12 '19 at 17:42
  • @Damon The counter index may be passed as a function argument as well, but I prefer this solution. First, a compiler may generate a slightly more efficient code since it knows the index at compile time. But more importantly, the index check can be done at compile time as well, which is much better than throwing some runtime error. You can also wrap the template stuff into a separate function, see the updated answer. – Daniel Langr Aug 12 '19 at 19:27
  • And like that, it even _looks_ pleasant, awesome. – Damon Aug 13 '19 at 07:37