简体   繁体   中英

C++ check type of template instance variable

I have use case similar to this question

I want to check what type of instance variable is stored in parameter without throwing an exception

class ParameterBase
{
public:
    virtual ~ParameterBase() {}
    template<class T> const T& get() const; //to be implimented after Parameter
    template<class T, class U> void setValue(const U& rhs); //to be implimented after Parameter
};

template <typename T>
class Parameter : public ParameterBase
{
public:
    Parameter(const T& rhs) :value(rhs) {}
    const T& get() const {return value;}
    void setValue(const T& rhs) {value=rhs;}    
private:
    T value;
};

//Here's the trick: dynamic_cast rather than virtual
template<class T> const T& ParameterBase::get() const
{ return dynamic_cast<const Parameter<T>&>(*this).get(); }
template<class T, class U> void ParameterBase::setValue(const U& rhs)
{ return dynamic_cast<Parameter<T>&>(*this).setValue(rhs); }

class Diagram
{
public:
    ParameterBase* v;
    int type;
};

What I want to be able to do is something like this


if (diagram.getParameter().type == int) {
}


How can I change this implementation so it will allow me to peek what type of Parameter is holding

Thanks for the answers , few more points

I am on C++ 11 so cannot use variant or any

Is there standard way of doing this. All I want is an instance variable of class that can be of multiple types (bounded) and while reading it check what type it is

The Simple Fix

The simple solution to your problem is to add a template function is<T>() to your ParameterBase that is defined in terms of dynamic_cast on a pointer. dynamic_cast with pointers return nullptr on failure, unlike references which will throw a std::bad_cast . For example:

class ParameterBase
{
public:
    ...
    template <typename T>
    bool is() const;
};

...

template <typename T> 
bool ParameterBase::is() const
{ 
    return dynamic_cast<const Parameter<T>*>(this) != nullptr;
}

The use would be simply:

if (diagram.getParameter().is<int>()) {
    ...
}

Note, however, that this whole design is not particularly nice. It has a cyclic dependency between the base and derived in a way that is highly coupled. Additionally it requires ParameterBase to exist as a pointer in order to operate correctly; where value-semantics would be much more coherent (if possible)

It would be better if you can use type-erasure, even if you define Parameter in terms of it (this is what C++17's std::any will do for you). The second answer in your linked question already describes what this may look like.


Type-erased Solution ( )

This uses C++11 features like forwarding references, rvalue-references, and unique_ptr -- but the concept can also be applied to earlier C++ versions.

For type-erasure, you would need an interface that encompasses at least these 2 features:

  • getting a reference to the templated type, and
  • getting an identifier for the current type.

Since interfaces in C++ can't be virtual , we have to get creative about returning the reference. C++ has void* which can be any kind of pointer. This can be bad if misused (such as casting between the wrong type); but if we know the underlying type, can be perfect. Thankfully here, we know the underlying type.

A quick form of type-erasure could be achieved with the following:

#include <type_traits> // std::decay
#include <utility>     // std::forward
#include <typeinfo>    // std::type_info, std::bad_cast
#include <memory>      // std::unique_ptr

class Parameter
{
private:

   // This is the interface we will implement in all instances
   struct Interface {
       virtual ~Interface() = default;
       virtual void* get() = 0;
       virtual const std::type_info& type() const = 0;
   };

   // This is the concrete instantiation of the above interfaces
   template <typename T>
   struct Concrete : public Interface {
       template <typename U>
       Concrete(U&& u) : m_value{std::forward<U>(u)} {}

       void* get() { return &m_value; }
       const std::type_info& type() const { return typeid(T); }

       T m_value; // actually holds the value here
   };

   // This holds onto the interface, and only the interface
   std::unique_ptr<Interface> m_interface;

public:

   // Constructs a parameter and sets the first interface value
   template <typename T>
   explicit Parameter(T&& value)
       : m_interface{new Concrete<typename std::decay<T>::type>{std::forward<T>(value)}}
   {}
   Parameter(Parameter&&) = default;
   Parameter& operator=(Parameter&&) = default;

   // Check if we are the same type by comparing the typeid
   template <typename T>
   bool is() const {
       return typeid(T) == m_interface->type();
   }

   // Get the underlying value. Always check that we are the correct type first!
   template <typename T>
   const T& get() const {
       // do the error handling ourselves
       if (!is<T>()) { throw std::bad_cast{}; }

       // cast void* to the underlying T*. We know this is safe
       // because of our check above first
       return (*static_cast<T*>(m_interface->get()));
   }

   // Set the underlying value. Always check that we are the correct type first!
   template <typename T, typename U>
   void set(U&& value) {
       // do the error handling ourselves
       if (!is<T>()) { throw std::bad_cast{}; }

       (*static_cast<T*>(m_interface->get())) = std::forward<U>(value);
   }

};

In the above, we take on the burden of detecting the underlying type ourselves -- but we remove the cyclic coupling. We now also have a proper value-type that we can move around like a normal variable, which is really helpful since it allows us to return this object from APIs without worrying about lifetime or ownership.

If copyability is desired as well, the interface can be extended to have a clone() function or something to return copies

Using this object, the code becomes:

if (parameter.is<int>()) {
    /* treat parameter as an int */
} 

Here's a small working example .


Type-erased Solution ( )

If you're looking for a finite set of instantiations, std::variant may be used for this purpose. If the number of possibly underlying types is unbounded , you should look into std::any

In either case, the use of a hierarchy here is superficial (at least in the current example) since the entire type-erasure can be reduced to a singular type with the ability to query the containment. This could be done easily, using std::any as an example:

#include <any> // std::any, std::any_cast

class Parameter
{
public:

    // This implementation changes the active type if 'T' is not the same as the stored
    // value. If you want to restrict this, you can do error checking here instead.
    template <typename T>
    void set(const T& value) { m_value = value; }

    template <typename T>
    const T& get() { return std::any_cast<const T&>(m_value); }

    template <typename T>
    bool is() const noexcept { return m_value.type() == typeid(T); }

private:

    std::any m_value;
};

If you don't want the active member to change, this could be restricted by checking is<T>() first and handling the error somehow.

Querying the active type can be achieved simply by doing:

if (parameter.is<int>()) {
    /* treat parameter as an int */
} 

If the types are fixed, you can always use std::variant instead using std::has_alternative for the definition of is

Looks like you know in advance all possible types for the parameter (I'm saying that because you have a type field that is expected to be used as an enumeration). If that is the case, you may use the std::variant idiom:

class Diagram
{
public:
    std::variant<Parameter<int>, Parameter<std::string>> v;
};

In this case you may use this code to get known the actual type:

switch(v.index()) {
case 0:
    // int is used
case 1:
    // string is used
}

For sure there are other alternatives. For example, if you have something of a type and you need to test if that is the type expect, you my use std::is_same template:

template <typename T>
class Parameter : public ParameterBase
{
public:
    bool isOfTypeInt() const {
        return std::is_same_v<T, int>;
    }
private:
    T value;
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM