dissecting any

In one of the previous posts we saw boost::any, it is one of the very good features to study. It promotes dynamic programming in C++, where you can change types at runtime, alternatively known as type-erasure.

It is quite easy to implement and exposes a very nice side of templates. It banks on polymorphism and some cool template usage. Let’s start the dissection, rather lets construct the feature from bottom up.

 

 

implementing any

 

We know that any derived class can go as base class (public base class in C++). This is also know as principle of substitution, see here and here for more details. This also applies for templated derived classes. Here is what I mean:

class Base
{
protected:
    Base()
    {}

public:
    
    virtual int NumOfTemplateParameters() const = 0;
    
    virtual ~Base()
    {}
};

template<typename T>
class DerivedType1 : public Base
{
public:
    DerivedType1()
    {}

    //  even if this is not called, being virtual code for this method
    //  will be generated
    //
    int NumOfTemplateParameters() const
    {
        return 1;
    }

    //  unless called code for this method will not be generated
    //  optimization with templates
    //
    template<typename U>
    void Print()
    {}
};

template<typename T1, typename T2>
class DerivedType2 : public Base
{
public:
    DerivedType2()
    {}

    int NumOfTemplateParameters() const
    {
        return 2;
    }
};

int main()
{
    Base* b1_1 = new DerivedType1<int>();
    Base* b1_2 = new DerivedType1<double>();

    Base* b2_1 = new DerivedType2<int,int>();
    Base* b2_2 = new DerivedType2<int,double>();
    Base* b2_3 = new DerivedType2<double,int>();
    Base* b2_4 = new DerivedType2<double,double>();

    delete b1_1;
    delete b1_2;
    delete b2_1;
    delete b2_2;
    delete b2_3;
    delete b2_4;
    
    return 0;
}

DerivedType1 and DerivedType2 template classes are subclasses of Base. But after template instantiation they behave as normal classes. However there are few things to note: any template function that is not called is not generated during template instantiation, i.e., template function Print is not called, so the method is not instantiated when DerivedType1 class with int parameter is generated. But the virtual function NumOfTemplateParameters is always generated, whether it is called or not, why? virtual functions are stored in vtable and their address is required so they need to be generated.

Ok so the aftermath is any non-templated class can be subtyped by a templated class. This gives us a very powerful way to have any kind of data in a structured hierarchy. Also a class can have a templated constructor, hence it can be created with any kind of data, like:

 

class NCtors
{
public:
    template<typename T>
    NCtors(const T&)
    {}

    template<typename T1, typename T2>
    NCtors(const T1&, const T2&)
    {}

};

int main()
{    
    int i = 0;
    double d = 0;
    char c = 'a';
    float f = 1.0;

    NCtors n1_1(i);
    NCtors n1_2(d);
    NCtors n1_3(c);
    NCtors n1_4(f);

    NCtors n2_1(i,i);
    NCtors n2_2(i,d);
    NCtors n2_3(d,i);
    NCtors n2_4(i,c);
    NCtors n2_5(f,c);
    // and so-on

    return 0;
}

NCtors has templated constructors, with one and two parameters, but it is not a template class, hence it can be created with any type of objects as shown above. any should be able to take in any type and store it without being templated. With templates it would have been quite easy to implement any, but we want it to stay as non-template since it is just a wrapper class that encapsulates any and every kind of data. So now lets compose the two bits we saw together to construct any!

 

//  required for typeid
#include <typeinfo>
#include <iostream>

class any
{
    class any_holder_base
    {
    public:
        friend class any;
        any_holder_base()
        {}

        virtual ~any_holder_base()
        {}

        virtual bool has_value(const std::type_info& typeInfo) const = 0;
    };

    template<typename T>
    class any_holder : public any_holder_base
    {
        friend class any;
        T t;
    public:
        any_holder(const T& val) : t(val)
        {}

        virtual bool has_value(const std::type_info& typeInfo) const
        {
            return typeInfo == typeid(T);
        }
    };


    //  pointer to base class, so that any derived can fit here
    //
    any_holder_base* impl;

public:

    template<typename T>
    any(const T& t) : impl( new any_holder<T>(t) )
    {
    }

    ~any()
    {
        delete impl;
    }

    //  accessing values:
    //
    template<typename T>
    bool has_value() const
    {
        return impl->has_value(typeid(T));
    }

    template<typename T>
    T get_value() const
    {
        if( this->has_value<T>() )
        {
            any_holder<T>* holder = static_cast<any_holder<T>*>(impl);
            return holder->t;
        }

        throw std::exception("bad casting");
    }
};

any acts as wrapper class, and any_holder_base acts as the base of the data container any_holder. any’s constructor is templated, there by taking any type of object and forwarding it to any_holder. The other interesting things are: about getting type of object stored and retrieving the object with correct type. Functions has_value and get_value do the job with the help of typeid operator and some static_casting.

 

Using the recently baked any:

int main()
{
    any any_int(1);
    any any_dbl(2.5);

    std::cout << "any_int has value of type int -- " << 
                std::boolalpha << any_int.has_value<int>() <<std::endl;
    std::cout << "any_int has value of type double -- " << 
                std::boolalpha << any_int.has_value<double>() <<std::endl;

    std::cout << "any_int has value " << any_int.get_value<int>() << std::endl;
    std::cout << "any_dbl has value " << any_dbl.get_value<double>() << std::endl;

    try
    {
        std::cout << "any_dbl has value " << any_dbl.get_value<int>() << std::endl;
    }
    catch(std::exception& err)
    {
        std::cout << err.what() << std::endl;
    }

    std::cin.ignore();
    return 0;
}

Well this is not production level code, cause we have not handled assigning and creating empty any objects. One way to do this is to use shared_ptr for impl and most of this behaviour comes for free, other way is to write lot of code to manage the memory and other cases. I would go for shared_ptr usage.

any, literally, takes anything. But at times you may want to constrain it to just specific usage, like any thing that is callable, should get in there by subverting the type-system but honoring the semantics. polymorphic function objects in C++ is a great example of this technique. May be in next post we will implement basic polymorphic function objects. Till then see you.

Tagged , . Bookmark the permalink.

Comments are closed.