Karlovsky120
Karlovsky120

Reputation: 6362

Creating an effective duplicate of a class that cannot interact with its base

Unreal Engine has a class called FVector which is a somewhat simple three component float vector.

One of the things that can be represented with that vector are coordinates. However, those coordinates can be in global space and local space.

There is a problem in the code base that I'm working on that it's not exactly clear which type of coordinate which functions take and it can be difficult to discern in what space a particular coordinate variable is when reading a random snippet of code.

I would like to have two types that behave the same way as FVector that represent those two cases, but cannot interact with one another, except through a explicit conversion function.

This way I can ensure that functions that take coordinates are explicit in what type of coordinate they take and the wrong type of coordinate cannot be passed to them by accident. Also when doing calculations with the coordinates, this would ensure that you cannot do operations between the two different types on accident.

What would be the best approach to this without outright duplicating the whole FVector class?

The solution should have minimal overhead and shouldn't make writing the code harder (when working with those types, users shouldn't really notice that they aren't FVectors until they try to pass them to the wrong function or add to a FVector). I'd like to avoid modifying the FVector base class, but would be willing to do it as long as behavior of FVector outside interacting with those types wouldn't change.

Upvotes: 4

Views: 131

Answers (2)

JeJo
JeJo

Reputation: 32972

What would be the best approach to this without outright duplicating the whole FVector class?

You can provide a type aliases for the template class, which inherits from the FVector, as follows:

namespace internal
{
    // Main template class for coordinates
    template<typename SpaceTag>
    class TCoordinate final : public FVector
    {
    public:
        using FVector::FVector; // Inherit constructors
        // ... Add addtional FVector operations as needed
    };
}
// Type aliases for cleaner usage
using FGlobalCoordinate = internal::TCoordinate<struct GlobalSpaceTag>;
using FLocalCoordinate = internal::TCoordinate<struct LocalSpaceTag>;

// Conversion functions
FLocalCoordinate GlobalToLocal(const FGlobalCoordinate& Global);
FGlobalCoordinate LocalToGlobal(const FLocalCoordinate& Local);

See a live demo


Keep in mind that FVector might not have a virtual destructor(Most likely this would be the version OP is talking about), therefore assigning the allocated memory for the above two classes to FVector wouldn't be a good idea. Read more here: Is it okay to inherit implementation from STL containers, rather than delegate?

In that case, you have to use the FVector should be a data member to the TCoordinate class and delegate all the members required.

Upvotes: 3

YSC
YSC

Reputation: 40150

If you accept a slight modification of the FVector class, this is manageable and compatible with your requirement:

[I] would be willing to [modify FVector] as long as behavior of FVector outside interacting with those types wouldn't change.

Full program demonstration:

//
// === FVector ===
//
// FVector class & associated functions to be modified for FVector to be a template class:
template<class tag=void>
struct FVector
{
    FVector& operator=(FVector<> const&) { return *this; }
};
template<class tag=void>
FVector<tag> operator+(FVector<tag> const&, FVector<tag> const&) { return {}; }


//
// === Local and Global vector classes ==
//
// FlavouredFVector class:
template<class tag>
struct FlavouredFVector : FVector<tag>
{
    FlavouredFVector() = default;
    template <class other>
    FlavouredFVector(FVector<other> const& b) : FVector<other>(b) {}
    explicit constexpr operator FVector<>() { return *this; }
};

// Global & Local flavours:
using LocalFVector = FlavouredFVector<struct this_is_a_local_vector>;
using GlobalFVector = FlavouredFVector<struct this_is_a_global_vector>;


//
// === Usage ===
//
// Free functions on vectors and flavoured vectors:
void vanilla(FVector<>) {}
void local(LocalFVector) {}
void global(GlobalFVector) {}

int main()
{
    // FVector usable as usual
    FVector v1,v2,v3;
    v1=v2+v3;
    vanilla(v1);

    // LocalFVector usable as if it were an FVector:
    LocalFVector l1,l2,l3;
    l1=l2+l3;
    local(l1);

    // GlobalFVector usable as if it were an FVector:
    GlobalFVector g1,g2,g3;
    g1=g2+g3;
    global(g1);
    vanilla(static_cast<FVector<>>(g1));

    // Incompatibility between LocalFVector & GlobalFVector at compilation time:
    l1=g1;          // error: type 'FVector<this_is_a_global_vector>' is not a direct base of 'FlavouredFVector<this_is_a_local_vector>'
    (void) (l1+g2); // error: no match for 'operator+' (operand types are 'LocalFVector' {aka 'FlavouredFVector<this_is_a_local_vector>'} and 'GlobalFVector' {aka 'FlavouredFVector<this_is_a_global_vector>'})
    local(g1);      // error: type 'FVector<this_is_a_global_vector>' is not a direct base of 'FlavouredFVector<this_is_a_local_vector>'
    vanilla(g1);    // error: could not convert 'g1' from 'GlobalFVector' {aka 'FlavouredFVector<this_is_a_global_vector>'} to 'FVector<>'
}

Demo on Compiler Explorer

Upvotes: 0

Related Questions