3

I posted this answer. Code:

#include <atomic>
#include <utility>
void printImpl(...);

std::atomic<bool> printLog = false;

class Log {
 public:
  template <typename T>
  const auto& operator<<(T&& t) {
    if (printLog) {
      ulog.active = true;
      return ulog << std::forward<T>(t);
    } else {
      ulog.active = false;
      return ulog;
    }
  }

 private:
  struct unchecked_log {
    template <typename T>
    const auto& operator<<(T&& t) const {
      if (active) {
        printImpl(std::forward<T>(t));
      }
      return *this;
    }
    bool active{false};
  };
  unchecked_log ulog{};
};

// Instead of the macro. Doesn't break backward compatibility
Log LOG;

void test(bool) { LOG << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10; }

In essence, the code either ignores or logs all the data. The idea was to record the atomic<bool> in a regular bool which can be optimized out more easily. I thought most compilers could easily optimize out the if (active) part since there is no way it can change between calls. Turns out though, most compilers do inline the function call to unchecked_log::operator<< but do not optimize out the branching. Is there something preventing this optimization? Would it be illegal.

Ayxan Haqverdili
  • 23,309
  • 5
  • 37
  • 74

1 Answers1

2

LOG is a global variable with external linkage. Therefore printImpl's definition in another translation unit can reach it and can potentially modify LOG.ulog.active between calls.

Make LOG a local variable in test and the repeated checks will be consolidated into one at the entry of test or leave LOG where it is and make it static, so a different compilation unit containing printImpl's definition cannot reach this translation unit's instance.

As mentioned in a comment below, alternatively let operator<< return by copy, so that the instance it returns (now a temporary) is unreachable for printImpl.


Note that the accessibility (private, etc.) of ulog and ulog.active does not matter. As soon as printImpl is able to obtain a pointer or reference to the instance of relevance, private does not protect from modification no matter what. Here are few examples of how that is possible (non-exhaustive):

  1. Calling operator<< on LOG which may now change LOG.ulog.active based on an intervening modification of printLog or by const_casting the result
  2. Calling the defaulted copy/move assignment operators
  3. (since this is a standard-layout class) reinterpret_cast LOG to a reference to its ulog member
  4. (since the classes are trivially copyable) memcpy a different state into LOG
  5. placement-new a new instance of Log into LOG, which will make previous references reference the new object automatically, because Log satisfies the conditions for that
  6. etc.
walnut
  • 21,076
  • 4
  • 21
  • 58
  • `LOG` has to be a global variable, but returning `LOG.ulog` by value [does enable the optimization](https://godbolt.org/z/mA-eg5) too. – Ayxan Haqverdili Dec 11 '19 at 18:10
  • `printImpl` could call `LOG.operator< – Ayxan Haqverdili Dec 11 '19 at 18:19
  • @Ayxan But, every time you write a `const_cast`, a kitten dies. Never do that. `reinterpret_cast` is not much better.. – Jesper Juhl Dec 11 '19 at 18:23
  • @Ayxan The point is that the *`unchecked_log` instance* returned by-value is not reachable by `printImpl`, it has no way of getting a pointer or reference to it. If it has a pointer or reference, then it basically doesn't matter how you tried to protect yourself with `private` and friends, there are ways to modify members even if you don't have access to their names. (It doesn't matter for this case, I just mentioned it as an aside.) – walnut Dec 11 '19 at 18:23
  • @walnut I think `reinterpret_cast` method wouldn't work because `Log::unchecked_log` is a private class, so there is no way to name it in `reinterpret_cast(&LOG)`, hence the `const_cast` method was the only thing preventing the optimization. – Ayxan Haqverdili Dec 11 '19 at 18:28
  • @Ayxan You can `decltype` for it though. Anyway, I believe walnut means that you can `reinterpret_cast` to access the member if you get an instance of `unchecked_log`. – François Andrieux Dec 11 '19 at 18:29
  • @FrançoisAndrieux You're right. So the `printImpl` could do something like `auto l = LOG << 1; auto& ulog = *(reinterpret_cast(&LOG)); ulog.active = true;` So what compilers did was illegal? – Ayxan Haqverdili Dec 11 '19 at 18:34
  • @Ayxan If you return by-value or use a `LOG` without external linkage, then `printImpl` has no way of obtaining a reference to the instance of relevance, so it cannot use that. These hypotheticals are for the case where you keep using the global `LOG.ulog` with external linkage. Therefore the optimization is valid. – walnut Dec 11 '19 at 18:36
  • @walnut In [this](https://godbolt.org/z/cGWYRb) example `LOG` is still a global variable. So `printImpl` could still do all the evil things you mentioned in the answer and I mentioned in [this](https://stackoverflow.com/questions/59291618/is-this-a-missed-optimization-opportunity-or-not#comment104788826_59291693) previous comment. But `if (active)` seems to be optimized out. Why is it optimized then? Am I getting something wrong? I am really sorry if there is something obvious I am missing. – Ayxan Haqverdili Dec 11 '19 at 18:56
  • 1
    @Ayxan Yes `printImpl` can modfiy `LOG.ulog.active`, but that is not what you keep using in `test`. The first `< – walnut Dec 11 '19 at 19:02
  • @JesperJuhl I've written `const_cast` when I have a pair of functions, one of which takes a const pointer and returns another const pointer which is related, and another which takes a non-const pointer and returns a similar non-const pointer. The non-const calls the const function, and const-casts the result. No kittens harmed. – Martin Bonner supports Monica Dec 11 '19 at 19:05
  • @walnut Oh, now I get it. Thank you very much for the effort and time you spent on this question. – Ayxan Haqverdili Dec 11 '19 at 19:06
  • @MartinBonner Well, as long as none of the objects you cast const off were originally declared as const (can be hard to know) and they are not modified, then there's no UB and the kittens live. I'd still call it questionable practice though. – Jesper Juhl Dec 11 '19 at 19:07
  • 1
    @Ayxan Note however that since you are calling `printImpl` first, before constructing the return value, there will actually be two branches (see the assembly). It is not entirely optimal. To make it optimal you should first make a copy of `ulog`, that you then return later or make `ulog` entirely local to the `operator< – walnut Dec 11 '19 at 19:08