1

Consider the following snippet of code:

#include <new>
#include <iostream>

struct IDivideResult {
    virtual int result() = 0;
    virtual int remainder() = 0;
};

struct DivideResult : IDivideResult {
    DivideResult(int result, int remainder) : result_(result), remainder_(remainder) {}
    int result() override { return result_; }
    int remainder() override { return remainder_; }

    int result_, remainder_;
};

struct LazyDivideResult : IDivideResult {
    LazyDivideResult(int dividend, int divisor) : dividend_(dividend), divisor_(divisor) {}
    int result() override { return Transmogrify()->result(); }
    int remainder() override { return Transmogrify()->remainder(); }

    DivideResult *Transmogrify() {
        int result = dividend_ / divisor_;
        int remainder = dividend_ % divisor_;
        return new (this) DivideResult(result, remainder);
    }

    int dividend_, divisor_;
};

void Print(IDivideResult *div) {
    int result = div->result();
    int remainder = div->remainder();

    std::cout << result << " " << remainder << "\n";
}

int main() {
    IDivideResult *div = new LazyDivideResult(10, 3);
    Print(div);
}

The question I have is about behavior of Print function.

Expected behavior: after calling result() div points to instance of DivideResult class, calling remainder() function calls DivideResult::remainder() function.

Possible (?) behavior: at the moment of calling result() pointer to vtable of LazyDivideResult is cached. The next call to remainder() reuses previously cached pointer to vtable and hence calls LazyDivideResult::remainder().

I've heard that virtual tables are not part of C++ standard. Also disassembly of clang/gcc/msvc generated code renders expected behavior.

So question here is: is compiler allowed to generate code that leads to "possible behavior" described above? Are there any guarantees?

pure cuteness
  • 1,635
  • 11
  • 26
  • 2
    C++ compilers are not required to use vtables to implement polymorphism. It just turns out that almost all of them do. But if the compiler can see an optimisation that can eliminate them, and still lead to correct behaviour, it's allowed to do it. This might help: https://stackoverflow.com/questions/99297/how-are-virtual-functions-and-vtable-implemented – Rags Oct 16 '18 at 09:35
  • 2
    Are you interested in knowing how to do this _without_ UB, or only in confirming that this specific code is indeed awful and wrong? – Useless Oct 16 '18 at 09:58
  • I'm aware of methods how to implement this without this "substitution" trick. I'm asking here about compilers behavior. And it seems that the answer is "behavior is undefined, refer to compiler documentation/code". – pure cuteness Oct 16 '18 at 10:08
  • Also I've seen these kind of tricks in [cppreference](https://en.cppreference.com/w/cpp/utility/launder) examples for `std::launder` function. So maybe it is not that much "undefined"? – pure cuteness Oct 16 '18 at 10:09
  • You'd need to launder the pointer _outside_ the function to get the guarantee you're hoping for. – Useless Oct 16 '18 at 10:12
  • What you mean? Should I call `Print(launder(div))`? But it does not make sense, since `div` points to different instance only after calling `result()` function. Or should I call `remainder()` function like this: `launder(div)->remainder()`. Also deletion of object (in `main` function) must be performed this way: `delete launder(div)`? Will any of that remove undefined behavior? – pure cuteness Oct 16 '18 at 10:21
  • @purecuteness You have a number of `IDivideResult *` (or derived) pointers. In `main` you have `div`, in `Print` you have `div` and in `result` and `remainder` you have `this`. Each has to be properly laundered, after each `Transmogrify`, before it can be used again. – Caleth Oct 16 '18 at 12:16

1 Answers1

1

This is undefined behaviour.

Once you call div->result(), the pointer div becomes invalid, because you have ended the lifetime of the object it points to. The symptom you observe is that it "succeeds" in calling a remainder.

In theory, an implementation could assume that you only ever pass DivideResult to Print, because passing a LazyDivideResult would be UB.

Caleth
  • 52,200
  • 2
  • 44
  • 75