c++inheritancec++17non-virtual-interface

Make base NVI virtual function private?


Imagine I have some NVI interface that allows for customization points via virtual functions.
And those virtual functions are designated solely for customization within the NVI and should never be invocable outside its context (explicitly called by derived classes elsewhere).

Making those functions private disallows the inheriting classes to call it explicitly (which is intended). But it also prevents the class from calling a default implementation (which is not intended).

I know I can write a comment to say "never call the base function outside the overriden function", but it leaves the ways to violate the interface.

Here's a small example:

class nvi
{
public:
    void operator()(int i) const {
        // do stuff
        _customization(i);
        // other stuff
    }

protected:
    ~nvi() = default;

private:
    virtual void _customization(int) const {}
};

class custom final : public nvi
{
public:
    void some_unrelated_func() {
        // this should not be possible
        // base::_customization(815);
    }

private:
    void _customization(int i) const override {
        // do custom stuff

        // this should be possible
        nvi::_customization(i);
    }
};

Is it possible to make the virtual function private in the base class and still be able to call in derived, but only in the overriden?


This is an XY question, but the X is not under my control here. Personally I think that having the function protected is ok when it doesn't mutate the state or have any observable side effects, so I wouldn't care if inherited classes would call explicitly call it somewhere else... Also, the possible solution should not be more complicated than the simple comment about the proper usage.


Solution

  • If you can modify nvi this is possible. Here's the full code, with an explanation to follow:

    #include <cstdio>
    
    class nvi
    {
    public:
        void operator()(int i) const {
            // do stuff
            _customization(i, private_tag{});
            // other stuff
        }
    
    protected:
        class private_tag {
            explicit private_tag() = default;
            friend class nvi;
        };
    
        ~nvi() = default;
        virtual void _customization(int, private_tag) const {
            std::printf("Hello");
        }
    };
    
    class custom final : public nvi
    {
    public:
        void some_unrelated_func() {
            // Not possible, can't get private_tag from anywhere
            // nvi::_customization(815, private_tag{});
        }
    
    private:
        void _customization(int i, private_tag key) const override {
            // do custom stuff
    
            // this is possible
            nvi::_customization(i, key);
            std::printf(" world!\n");
        }
    };
    

    The idea is basically to just use tag-dispatch. We define a type private_tag, which can be named or copied by any sub-class, but can only be constructed inside the scope of nvi and nowhere else. This idiom encodes "access rights" in values that we can pass around.

    So operator(int) creates a private_tag and passes it to _customization. Then it follow that any _customization override can either ignore the tag or it can pass it to some function that accepts it (such as nvi::_customization or _customization in any class between the final overrider and nvi).

    Other member functions can't call nvi::_customization (or even their own _customization1) because they have no way to create a private_tag. This gives us the semantics you want more or less.


    1 - Not really that limiting because we can always have _customization delegate to some private function that we *may* call.