Search code examples
c++classtemplatesspecialization

Implementing template specialized functions


I'm trying to allow a class to implement an interface, where the interface is templated to either allow an update internally or not. The code looks like this and works if the implementation is within the class declaration. If it is moved out (as shown here), I get a compiler error on Visual Studio 2019, and I can't understand why. All help is appreciated!

The following code will not compile. If the out of class implementation and headers are modified to just use the code I commented out, it compiles and works as expected.

#include <atomic>
#include <cstdint>
#include <memory>
#include <iostream>
#include <string>


enum class EntryType { Read, ReadUpdate };

template <EntryType Type>
class IXQ
{
public:
    virtual float GetValue() = 0;
};

class X : public IXQ<EntryType::Read>, public IXQ<EntryType::ReadUpdate>
{
public:
    float IXQ<EntryType::Read>::GetValue();
    /*
    float IXQ<EntryType::Read>::GetValue()
    {
        return _x;
    }
    */
    float IXQ<EntryType::ReadUpdate>::GetValue();
    /*
    float IXQ<EntryType::ReadUpdate>::GetValue() {
        _counter++;
        return _x;
    }
    */
    float _x = 10.0F;
    std::atomic<std::int32_t> _counter = 0;
};

float X::IXQ<EntryType::Read>::GetValue()
{
    return _x;
}

float X::IXQ<EntryType::ReadUpdate>::GetValue()
{
    _counter++;
    return _x;
};


int main(int argc, char* argv[])
{
    {
        auto k = std::make_unique<X>();

        auto ptrQ = static_cast<IXQ<EntryType::Read>*>(k.get());

        std::cout << std::to_string(ptrQ->GetValue()) << std::endl;

        std::cout << k->_counter.load() << std::endl;
    }

    {
        auto k = std::make_unique<X>();

        auto ptrQ = static_cast<IXQ<EntryType::ReadUpdate>*>(k.get());

        std::cout << std::to_string(ptrQ->GetValue()) << std::endl;

        std::cout << k->_counter.load() << std::endl;
    }


    return 0;
}

Solution

  • 1. Microsoft-specific syntax

    The syntax you're using to override GetValue() is not standard c++.

    class X : public IXQ<EntryType::Read> {
    public:
        float IXQ<EntryType::Read>::GetValue() { /* ... */ }
    }
    

    This is a Microsoft-Specific Extension for C++ called Explicit Overrides (C++) and is intended to be used with __interfaces (also a microsoft-specific extension to c++).

    __interfaces do not officially support c++ templates due to them being intended mostly for COM Interfaces, e.g.:

    [ object, uuid("00000000-0000-0000-0000-000000000001"), library_block ]
    __interface ISomething {
       // properties
       [ id(0) ] int iInt;
       [ id(5) ] BSTR bStr;
    
       // functions
       void DoSomething();
    };
    

    It's suprising that float IXQ<EntryType::Read>::GetValue() { /* ... */ } works at all.


    2. Standard C++-compliant solutions

    In standard C++ the overriden function is determined by the name & arguments alone, so your only option is to override both of them.

    e.g.:

    class X : public IXQ<EntryType::Read>, public IXQ<EntryType::ReadUpdate>
    {
    public:
        // will be the implementation for both IXQ<EntryType::Read> & IXQ<EntryType::ReadUpdate>
        float GetValue() override;
    };
    
    float X::GetValue() {
        /* ... */
    }
    

    Another standard-compliant way would be to create 2 classes that inherit from the given interfaces & then let X inherit from both of them:

    class XRead : public IXQ<EntryType::Read> {
    public:
        float GetValue() override { /* ... */ }
    };
    
    class XReadUpdate : public IXQ<EntryType::ReadUpdate> {
    public:
        float GetValue() override { /* ... */ }
    };
    
    class X : public XRead, public XReadUpdate {
        /* ... */
    };
    

    If you want to share state between XRead & XReadUpdate, you'd have to introduce another level, e.g.:

    class XBase {
    public:
        virtual ~XBase() = default;
    protected:
        float value;
    };
    
    class XRead : public virtual XBase, public IXQ<EntryType::Read> {
        float GetValue() override { return value; }
    };
    
    class XReadUpdate : public virtual XBase, public IXQ<EntryType::ReadUpdate> {
        float GetValue() override { return value; }
    };
    
    class X : public XRead, public XReadUpdate {
        /* ... */
    };
    

    3. Possible recommendations for API Design

    Keep in mind though that this type of API design will be rather complicated to use, because you always need to cast to the specific interface first before you can call the function because X{}.GetValue() would be ambigous.

    e.g.:

    X x;
    
    IXQ<EntryType::Read>& read = x;
    std::cout << read.GetValue() << std::endl;
    
    IXQ<EntryType::ReadUpdate>& update = x;
    std::cout << update.GetValue() << std::endl;
    
    // this is *not* possible, GetValue() is ambigous
    // std::cout << x.GetValue() << std::endl;
    

    I'd recommend separating both interfaces & using different method names, e.g.: godbolt example

    struct IXQReadonly {
        virtual ~IXQReadonly() = default;
        virtual float GetValue() = 0;
    };
    
    struct IXQ : IXQReadonly {
        virtual float GetValueUpdate() = 0;
    };
    
    class X : public IXQ {
    public:
        X() : value(0.0f), counter(0) { }
    
        float GetValue() override { return value; }
        float GetValueUpdate() override {
            ++counter;
            return value;
        }
    
    private:
        float value;
        std::atomic<int> counter;
    };
    

    This comes with a whole set of benefits:

    • You can call GetValue() / GetValueUpdate() directly on X, no need to convert to an interface first
      X x;
      x.GetValue();
      x.GetValueUpdate();
      
    • It is immediately clear from the method name if it will update the counter or not
    • A function that is given an IXQ can call a function that expects IXQReadonly, but not the other way round: (so you can "downgrade" a read-write interface to readonly, but not "upgrade" a readonly interface to read-write)
      void bar(IXQReadonly& r) { r.GetValue(); /* we can't do GetValueUpdate() here */ }
      void foo(IXQ& x) { bar(x); x.GetValueUpdate(); }
      

    Additionally remember to declare the destructor as virtual (at least in the top-most class / interface), otherwise funny things tend to happen if you try to delete an instance of X through a pointer to one of its bases / interfaces.