how to avoid CRTP "error: invalid use of incomplete type" for classes with all public static methods?

Viewed 47

the goal is several scoped enums (each in it own struct/class/namespace) with some associated utility functions. I've used that pattern many times and the utility functions always include a toString() and fromString() function for human intelligible i/o.

However, this time I've got several different types of dimensions: Length, Time, Angle, ... that I want to handle Unit's for so the naming pattern of these structs structs/classes/namespaces is Unit. And these different classes are practically identical except each one has an enum of units specific to that dimension.

for the LengthUnit struct/class/namespace the enum is currently

enum type
{
    meters=0,
    kilometers,
    SIZE
};

obviously I could add centimeters, millimeters, micrometers, miles, feet, yards etc, but I don't need those right now

for the TimeUnit struct/class/namespace the enum is

enum type
{
    seconds=0,
    milliseconds,
    microseconds,
    nanoseconds,
    SIZE
};

for the AngleUnit struct/class/namespace the enum is

enum type
{
    degrees=0,
    radians,
    SIZE
};

each Unit class has 4 identically (across dimensions) named functions

static std::string toString(type unit); 
static type fromString(std::string unitString); 
static std::string toAbbreviation(type unit);
static type fromAbbreviation(std::string unitString);

the toString() and toAbbreviation() functions are specific to the Type because each contains a custom switch statement to convert from unit to string.

the fromString() and fromAbbreviation() function have a common implementation, they loop over the enum lists essentially a for(type unit=0; unit<SIZE; ++unit) (with some static_cast-ing) to compare the string to the one coming out of toString() or toAbbreviation() when it finds a match it breaks, if it doesn't find a match it returns SIZE, errors are thrown exceptions

and each unit has function whose name is specific to the type of dimension, for example static double lengthInMeters(const type unit): //for unit=LengthUnit::meters this returns 1.0, for unit=LengthUnit::kilometers it returns 1000.0 static double timeInSeconds(const type unit); //for unit=TimeUnit::seconds this returns 1.0, for unit=TimeUnit::milliseconds it returns 1e-3, etc. static double angleInDegrees(const type unit);

to decrease boiler plate code (because the fromString() and fromAbbreviation() function have identical implementations across dimension types) I tried to template the common class and use the CRTP to do it, the relevant code you need to compile and reproduce the same error is

#include <algorithm>
#include <cctype>
#include <sstream>
#include <exception>
#include <cstring>

class StringException : public std::exception
{
protected:
    const std::string m_message;
public:
    StringException(const std::string& message) : m_message(message) {};

    virtual const char* what() const throw()
    {
        return m_message.c_str();
    };
};

inline std::string toLower(std::string str)
{
    std::transform(str.begin(), str.end(), str.begin(),
        [](unsigned char c){ return std::tolower(c); });
    return str;
};

struct DimensionType
{
    enum type
    {
        Length=0,
        Time,
        Angle,
        SIZE
    };

    std::string toString(const type dimType=SIZE)
    {
        std::ostringstream oss;
        switch(dimType)
        {
            case Length: return std::string("Length");
            case Time: return std::string("Time");
            case Angle: return std::string("Angle");
            case SIZE:
                oss << "SIZE=" << SIZE;
                return oss.str();
            default:
                oss << "DimensionType::toString():\n  "
                    << "We either encountered an invalid DimensionType value OR\n  "
                    << "DimensionType::toString() needs to be updated to handle a new DimensionType value\n";
                throw(StringException(oss.str()));
        }
    };

    type fromString(const std::string dimString)
    {
        for(type dimType=Length; dimType<SIZE; dimType=static_cast<type>(dimType+1))
        {
            try
            {
                if(dimString.compare(toString(dimType))==0)
                    return dimType;         
            }
            catch(std::exception& e)
            {
                std::ostringstream oss;
                oss << "DimensionType::fromString():\n  "
                    << e.what();
                throw(StringException(oss.str()));
            }
        }
        return SIZE;
    };
};

typedef DimensionType::type DimensionType_t;


template <class T>
class DimensionUnit 
{
public:
    static const std::string DimName;
    static const DimensionType_t DimType;

    static typename T::type fromString(std::string unitString)
    {
        unitString=toLower(unitString);
        for(typename T::type unit=static_cast<typename T::type>(0); unit<T::SIZE; unit=static_cast<typename T::type>(unit+1))
        {
            try
            {
                if(unitString.compare(typename T::toString(unit))==0)
                    return unit;                
            }
            catch(std::exception& e)
            {
                std::ostringstream oss;
                oss << DimName << "Unit::fromString():\n  "
                    << e.what();
                throw(StringException(oss.str()));
            }
        }
        return T::SIZE;
    };

    static typename T::type fromAbbreviation(std::string unitString)
    {
        unitString=toLower(unitString);
        for(typename T::type unit=static_cast<typename T::type>(0); unit<T::SIZE; unit=static_cast<typename T::type>(unit+1))
        {
            try
            {
                if(unitString.compare(typename T::toAbbreviation(unit))==0)
                    return unit;                
            }
            catch(std::exception& e)
            {
                std::ostringstream oss;
                oss << DimName << "Unit::fromAbbreviation():\n  "
                    << e.what();
                throw(StringException(oss.str()));
            }
        }
        return T::SIZE;
    };
};

class LengthUnit : public DimensionUnit<LengthUnit>
{
    public:
    enum type
    {
        meters=0,
        kilometers,
        SIZE
    };

    static double lengthInMeters(const type unit)
    {
        std::ostringstream oss;
        switch(units)
        {
            case meters: return 1.0;
            case kilometers: return 1000.0;
            case SIZE:
                oss << "LengthUnit::lengthInMeters():\n  "
                    << "SIZE is not a LengthUnit value.  It is a count of defined LengthUnit values\n  "
                    << "that enables iteration/looping.\n";
                throw(StringException(oss.str()));
            default:
                oss << "LengthUnit::lengthInMeters():\n  "
                    << "We either encountered an invalid LengthUnit value OR\n  "
                    << "LengthUnit::lengthInMeters() needs to be updated to handle a new LengthUnit value\n";
                throw(StringException(oss.str()));
        }       
    };

    static std::string toString(const type unit=SIZE)
    {
        std::ostringstream oss;
        switch(units)
        {
            case meters: return std::string("meters");
            case kilometers: return std::string("kilometers");
            case SIZE:
                oss << "SIZE=" << SIZE;
                return oss.str();
            default:
                oss << "LengthUnit::toString():\n  "
                    << "We either encountered an invalid LengthUnit value OR\n  "
                    << "LengthUnit::toString() needs to be updated to handle a new LengthUnit value\n";
                throw(StringException(oss.str()));
        }
    };

    static std::string toAbbreviation(const type unit=SIZE)
    {
        std::ostringstream oss;
        switch(units)
        {
            case meters: return std::string("m");
            case kilometers: return std::string("km");
            case SIZE:
                oss << "SIZE=" << SIZE;
                return oss.str();
            default:
                oss << "LengthUnit::toAbbreviation():\n  "
                    << "We either encountered an invalid LengthUnit value OR\n  "
                    << "LengthUnit::toAbbreviation() needs to be updated to handle a new LengthUnit value\n";
                throw(StringException(oss.str()));
        }
    };
};
typedef LengthUnit::type LengthUnit_t;
const std::string LengthUnit::DimName("Length");
const DimensionType_t LengthUnit::DimType(DimensionType::Length);
inline std::ostream& operator<<(std::ostream& os, LengthUnit_t unit)
{
    os << "( " << LengthUnit::toAbbreviation(unit) << ")";
    return os;
};

this is my first time attempting to use an CRTP because I've never had that kind of boilerplate similarity before and the errors I'm getting are of the form

error: invalid use of incomplete type ‘class LengthUnit’
  static typename T::type fromString(std::string unitString)
                          ^~~~~~~~~~
 note: forward declaration of ‘class LengthUnit’
 class LengthUnit : public DimensionUnit<LengthUnit>
       ^~~~~~~~~~
 error: invalid use of incomplete type ‘class LengthUnit’
  static typename T::type fromAbbreviation(std::string unitString)
                          ^~~~~~~~~~~~~~~~
 note: forward declaration of ‘class LengthUnit’
 class LengthUnit : public DimensionUnit<LengthUnit>

google seems to indicate I've got an infinite recursion problem, but I'm hoping there's a smallish change I could make to avoid it.

0 Answers
Related