简体   繁体   中英

C++ templates std::tuple to void* and back

I'm trying to make a resource manager using C++11 and variadic templates. The question is how to store std::tuple to collection and get it back? I've tried to store it to void* in this example (trying not to use boost::any here). Every time I'm casting back to std::tuple I'm getting that cast'ed tuple is the same as tuple created from the params (currentArgs == storedArgs). The code below I think explains everything.

http://ideone.com/h3yzvy

#include <memory>
#include <typeindex>
#include <iostream>
#include <string>
#include <vector>
#include <map>

typedef std::multimap<std::type_index, void*> Object;
typedef std::map<Object, std::shared_ptr<void>> ObjectCollection;

Object object;
ObjectCollection objectCollection;

template<typename T, typename... Args>
T* getResource(Args&& ... args)
{
    // Creating tuple from the arguments
    std::tuple<Args...> currentArgs(std::forward<Args>(args)...);
    // Getting object type info
    std::type_index type = { typeid(T) };

    // Getting all objects from the collection that are of the same type
    auto range = object.equal_range(type);

    for (auto it = range.first; it != range.second; ++it)
    {
        // it->second is a void* Since we are iterating through
        // the the collection of the same type I'm trying to cast
        // back. Object construct parameters should be the same 
        // (in this example: const std::string &fileName)
        auto storedArgs = *static_cast<std::tuple<Args...>*>(it->second);

        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        // Problem is here. currentArgs and storedArgs are always equal :/
        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        // Return the object from the collection if current arguments and
        // arguments from the collection are the same
        if (currentArgs == storedArgs)
        {
            std::cout << "Found... returning..." << std::endl;
            // found... return...
            return static_cast<T*>(objectCollection[object].get());
        }
    }

    // Object with the same arguments were not found
    // Adding to collection and return
    std::cout << "Adding to collection..." << std::endl;
    object.emplace(type, &currentArgs);
    objectCollection[object] = std::make_shared<T>(std::forward<Args>(args)...);
    return static_cast<T*>(objectCollection[object].get());
}

class Resource
{
public:
    virtual ~Resource() = default;

    template<typename T, typename... Args>
    static T* get(Args&& ... args)
    {
        return getResource<T>(std::forward<Args>(args)...);
    }
};

class Image
{
public:
    Image(const std::string &fileName)
    {
        std::cout << "Loading image " << fileName.c_str() << std::endl;
    }

    ~Image(){};
};

int main()
{
    auto image1 = Resource::get<Image>("aaa.jpg");
    auto image2 = Resource::get<Image>("bbb.jpg");
    auto image3 = Resource::get<Image>("aaa.jpg");
    getchar();
}

EDIT

Thanks everybody for the input. In case anyone cares my final Resource.h looks like this and works perfect:

#pragma once

#include <memory>
#include <map>

template<class T, class...Args>
std::map<std::tuple<Args...>, std::shared_ptr<T>>& getCache()
{
    static std::map<std::tuple<Args...>, std::shared_ptr<T>> cache; // only run once
    return cache;
}

template<typename T, typename... Args>
std::shared_ptr<T> getResource(Args&& ... args)
{
    // std::decay_t should be used
    auto& cache = getCache<T, std::decay_t<Args>...>();

    // Creating tuple from the arguments
    auto arguments = std::forward_as_tuple(std::forward<Args>(args)...);

    // Search for object in the cache
    auto it = cache.find(arguments);

    if (it != cache.end())
    {
        // Found. Return.
        return it->second;
    }

    // Not found. Add to cache.
    auto object = std::make_shared<T>(std::forward<Args>(args)...);
    cache.emplace(std::make_pair(std::move(arguments), object));
    return object;
}

class Resource
{
public:
    virtual ~Resource() = default;

    template<typename T, typename... Args>
    static std::shared_ptr<T> get(Args&& ... args)
    {
        return getResource<T>(std::forward<Args>(args)...);
    }
};

Why don't you use one function-local map per type and args? Since you are already filtering your data by these 2 conditions it could simplify your code:

#include <iostream>
#include <math.h>

using namespace std; 

#include <memory>
#include <typeindex>
#include <iostream>
#include <string>
#include <vector>
#include <map>

template<typename T, typename... Args>
std::shared_ptr<T> getResource(Args&& ... args)
{
    static std::map<std::tuple<Args...>, std::shared_ptr<T>> objectCollection;

    // Creating tuple from the arguments
    std::tuple<Args...> currentArgs(std::forward<Args>(args)...);

    //Search for object in map
    auto objectIter = objectCollection.find(currentArgs);

    if(objectIter != objectCollection.end())
    {
        std::cout << "Found... returning..." << std::endl;
        return objectIter->second;
    }

    std::shared_ptr<T> newObject(new T(args...));
    std::cout << "Adding to collection..." << std::endl;
    objectCollection.insert(std::pair<std::tuple<Args...>, std::shared_ptr<T>>(currentArgs, newObject));
    return newObject;
}

class Resource
{
public:
    virtual ~Resource() = default;

    template<typename T, typename... Args>
    static std::shared_ptr<T> get(Args&& ... args)
    {
        return getResource<T>(std::forward<Args>(args)...);
    }
};

class Image
{
public:
    Image(const std::string &fileName)
    {
        std::cout << "Loading image " << fileName.c_str() << std::endl;
    }

    ~Image() {};
};

int main()
{
    auto image1 = Resource::get<Image>("aaa.jpg");
    auto image2 = Resource::get<Image>("bbb.jpg");
    auto image3 = Resource::get<Image>("aaa.jpg");
    getchar();
}

EDIT: I also changed the code to use shared_ptr all the way through.

You are storing a pointer to a local variable of your function:

// declaration of local variable "currentArgs"
std::tuple<Args...> currentArgs(std::forward<Args>(args)...);

// ...

// storing the pointer of "currentArgs" in "object"
object.emplace(type, &currentArgs);

That local variable ( currentArgs ) lives on the stack and the pointer to it becomes invalid after returning from the function. By coincidence (because you call the function from the same place), the next time you call the function the address of the variable is exactly the same, which means dereferencing your (invalid) pointer resolves to the current value of currentArgs .

In order to avoid the problem, create a permanent object using new or make_shared and put the raw pointer or smartpointer to it in the map object .

In this line:

 object.emplace(type, &currentArgs);

The character array (or whatever type is passed in) will be on the stack. That is not storage you can own. It was not allocated for use and storage via any pointer, let alone void *, which means the content of that pointer is from the stack.

At each call in these lines:

   auto image1 = Resource::get<Image>("aaa.jpg");
   auto image2 = Resource::get<Image>("bbb.jpg");

And any others that follow, the stack happens to be in the same state before each call. This means when "bbb.jpg" is called, the 'emplace' call is pointing to the SAME memory, but that has now become "bbb.jpg" instead of "aaa.jpg". As the stack is used elsewhere in future versions of this program, the stack will be changing as a result of a running program, meaning the content of the stored object(s) will change, seemingly randomly.

What you must do is reconsider storage.

You could allocate a new copy of the object you want to store, but that presents another problem. You've stored a shared_ptr in ObjectCollection. It will not know how to delete that. In reality, the pointer "owned" by shared_ptr could be anything, including a C++ class or struct, which REQUIRES destruction (as in delete p, where p is the void * cast to said object). It can't know how to do that because shared_ptr only "knows" this to be a void *. It will only perform a delete of void *, and the destructor of that object will never be called. In order for that to be valid, you'd have to ensure only POD types (that don't require destructors be called) could be valid. Simply put, for the context you're using, you can't use shared_ptr as a means of ensuring disposal of the memory, because it's not just freeing memory, it's destruction you must deal with.

You could create copies of objects, which are not stored by void, but that means the map and multimap could not store just any object.

This is the purpose of boost::any, but if you can't use that, you have to reconsider how to either handle destruction of the object(s) in the maps, or you have to limit storage to types that don't require destructors.

There are too many potential solutions to the delimma to finalize a solution (I would be building the product for you, and making design choice for you to do that).

I can tell you the features required in the solution.

You must do away with shared_ptr. You can't rely on "automatic" freeing, which was your purpose in using shared_ptr. At destruction, you have no choice but to loop through all contained entries, cast them to their real types, and delete them "manually". How you do that has myriad possibilities.

Your code has a few fundamental errors.

First, you are using types deduced as forwarding references as if they where value types. Args&&... are deduced forwarding references, which means Args could be a value or reference type. std::tuple<Args> could then be a tuple of references. This is not what you want.

Second, you are trying to avoid boost::any , and then you are reimplementing it wrong. A boost::any is a void* and information about how to copy/destroy/cast it back to its original type. Simply storing a void* won't do; and storing a pointer to a variable of automatic storage (a stack variable) will be utter garbage.

A distinct map for each type entry is tempting, but a decent program needs to be able to clear them.

Here is a .clear() type erasure view object. It erases the action of calling .clear() on an object of arbitrary type:

struct clear_later {
  void*p = nullptr;
  void(*f)(void*) = nullptr;
  template<class O,
    std::enable_if_t<!std::is_same<std::decay_t<O>,clear_later>{}>* = nullptr
  >
  clear_later( O&& o ):
    p(std::addressof(o)),
    f([](void* p){
      auto*po = static_cast<std::decay_t<O>*>(p);
      po->clear();
    })
  {};
  clear_later(clear_later const&)=default;
  clear_later()=default;
  void operator()()const{
    if (f) f(p);
  }
  explicit operator bool()const{ return f; }
  template<class Self>
  friend auto make_tie(Self&&self){
    return std::tie( std::forward<Self>(self).p, std::forward<Self>(self).f );
  }
  friend bool operator<( clear_later lhs, clear_later rhs )const{
    return make_tie(lhs) < make_tie(rhs);
  }
};

Now we can build a set of caches to clear, where the caches are different types:

std::vector<clear_later> caches_to_clear;
void clear_caches() {
  for (auto&& clear:caches_to_clear)
    clear();
}

Now we need a way to automatically register the caches that are created. We also want to be able to look up "transparently", so we use std::less<void> for searching:

template<class T, class...Args>
std::map< std::tuple<Args...>, T, std::less<> >& make_and_register_cache() {
  static std::map< std::tuple<Args...>, T, std::less<>> retval; // actual storage
  caches_to_clear.emplace_back(retval);
  return retval;
}
template<class T, class...Args>
std::map< std::tuple<Args...>, T, std::less<>>& get_cache() {
  static auto& cache = make_and_register_cache(); // only run once
  return cache;
}

Finally:

template<typename T, typename... Args>
std::shared_ptr<T> getResource(Args&& ... args)
{
  // notice the use of decay.  This is important:
  auto& cache = get_cache<T, std::decay_t<Args>...>();

  // Creating tuple from the arguments (via forwarding)
  auto currentArgs = std::forward_as_tuple(std::forward<Args>(args)...);

  //Search for object in map
  auto objectIter = cache.find(currentArgs);

  if(objectIter != cache.end()) {
    std::cout << "Found... returning..." << std::endl;
    return objectIter->second;
  }

  // note lack of forward, and use of make_shared.  Never forward twice!
  auto newObject = std::make_shared<T>(args...);
  std::cout << "Adding to collection..." << std::endl;
  // get rid of extra copy of args you made here by calling emplace
  // move of forwarding tuple activates forwarding:
  cache.emplace(std::move(currentArgs), std::move(newObject));
  return newObject;
}

We can now add things to the cache. We don't store references in the cache (unlike your version). We can clear every cache by calling clear_caches .

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