c++dictionarytemplatesany

C++17 Dictionary of names to any


I'm trying to create a dictionary in C++17 which associates a name to a pointer of any number of types simultaneously, and is able to deduce back the type on lookup without resorting to explicit casts on the caller side. These types can't be specified upfront in a variadic template.

The desired interface is the following:

AnyDict<std::string, std::map> dict;

float num = 0.5f;
std::string str = {"Value"};

// If both inserts succeed this should evaluate to true.
if ( dict.insert("Hello"s, num) &&
     dict.insert("Other"s, str) )
    ; // ...

// This should return true.
dict.remove("Hello"s);

std::string def = {""};

// This should return a pointer to "Other", but
// if something goes wrong returns "".
auto* value = dict.find("Other"s, &def);

Currently my closest solution involves:

// Currently I'm not using dlls or anything, but...
class TypeIdx
{
public:
    template <class Type>
    static int get()
    {
        // ugly.
        static const int s_idx = s_count++;

        return s_idx;    
    }

private:
    static inline int s_count = 0;
};
template <template <class, class> class Dict>
class AnyDict
{
public:
    // constructors, destructor, etc.

    template <class Type>
    bool
    insert(Type& item)
    {
        int key = TypeIdx::get<Type>();
        
        // The delegate must return true on success
        // or false on failure.
        return m_dict.insert(key, (void*) &item);
    }

    template <class Type>
    bool
    remove()
    {
        int key = TypeIdx::get<Type>();
        
        // The delegate must return true on success
        // or false on failure.
        return m_dict.remove(key);
    }

    template <class Type>
    Type*
    find(Type* def)
    {
        int key = TypeIdx::get<Type>();
        
        // The delegate must return a pointer to
        // the item found on success, or to "def"
        // on failure.
        return (Type*) m_dict.find(key, def);
    }

private:
    // Here the void* lets me save a pointer to
    // anything but i can't deduce back the type.
    Dict<int, void*> m_dict;
};

While this works and returns pointers of the correct type without casts, it still doesn't index them by name but by type, which is not what I'm looking for.

Do you know any solution? Maybe using std::any would improve the situation? Also if you could suggest a better name for this class it would be appreciated.


Solution

  • I think you should use std::any as the value type of your map, because it implements exactly the kind of type erasure that you need. No need to bother with scary void pointers. Here is a solution to get you started:

    #include <any>
    #include <string>
    #include <map>
    #include <iostream>
    
    
    template <typename key_type, template<typename, typename> typename Map>
    class AnyDict
    {
    public:
    
        AnyDict() = default;
    
        template <typename value_type>
        bool insert(key_type const& k, value_type const& v) {
            auto [it, rt] = m_values.insert(std::make_pair(k, std::any{v}));
            return rt;
        }
    
        template <typename value_type>
        value_type find(key_type const& k, value_type const& default_value) const 
        {
            if (auto it = m_values.find(k); it != m_values.end()) {
    
                if (it->second.type() != typeid(value_type)){
                    throw std::logic_error("type mismatch");
                }
    
                return std::any_cast<value_type>(it->second);
            }
            else {
                return default_value;
            }
        }
    
        bool remove(key_type const& k) {
            if (auto it = m_values.find(k); it != m_values.end()) {
                m_values.erase(k);
                return true;
            }
            return false;
        }
    
    private:
    
        Map<key_type, std::any> m_values;
    };
    
    int main()
    {
        AnyDict<std::string, std::map> dict;
    
        // If both inserts succeed this should evaluate to true.
        if ( dict.insert(std::string {"Hello"}, 0.5f) &&
            dict.insert(std::string {"Other"}, std::string {"Value"}) )
        {
            std::cout << "Success.\n";
        }
    
        // This should return true.
        std::cout << std::boolalpha << dict.remove(std::string {"Hello"}) << "\n";
    
        // This should return "Other", but if
        // something goes wrong returns "".
        auto value = dict.find(std::string {"Other"}, std::string {""});
        std::cout << value << "\n";
    
    };
    

    https://godbolt.org/z/GsrTE7975

    Now I think there are some minor discrepancies between your desired interface and your question. For one, the desired interface doesn't store any pointers, but values, that is, according to the interface, the dict owns the values. The code above let's you store any type, including, but not limited to pointers: https://godbolt.org/z/sfhT68xK9