Search code examples
c++templatesperfect-forwarding

What is the cost of calling a member function of a class through a temporary object if the class doesn't have any member variables?


I was recently studying the source code of the ENTT library, and I came across something similar to the following snippet of code (note that I have greatly simplified things to make my question brief):

// Note that this class doesn't contain any member variables
class TextureLoader
{
public:

   TextureLoader() = default;
   ~TextureLoader() = default;

   std::shared_ptr<Texture> loadResource(const std::string& textureFilePath) const;
};

template<typename TResource, typename TResourceLoader, typename... Args>
std::shared_ptr<TResource> loadResource(Args&&... args)
{
   // Note how a temporary TResourceLoader is created to invoke its loadResource member function
   return TResourceLoader{}.loadResource(std::forward<Args>(args)...));
}

int main()
{
   std::string texFilePath = "tex.png";
   std::shared_ptr<Texture> myTexture = loadResource<Texture, TextureLoader>(texFilePath);
   return 0;
}

As you can see, the loadResource function template is capable of loading any resource type (e.g. Texture, Shader, Model, Sound, etc.). The documentation of the library states that a loader class should ideally not contain any member variables. I imagine this is because every time loadResource is called, a temporary of the loader class passed to it is created to invoke its loadResource member function. And that's where my question lies: what is the cost of TResourceLoader{}.loadResource()? Is the compiler able to remove the creation of the temporary because it doesn't contain any member variables? Is there a better way to this?


Solution

  • There should be no significant performance implications, although code will be penalized ever so slightly. In order to understand the implications better, let's try to decompose the code into something which would be similar to compiler's generated code:

    From:

    return TResourceLoader{}.loadResource(std::forward<Args>(args)...));
    

    To:

    char Storage[1]; // Any object in C++ is at least 1 byte, including classes with no members
    Storage(&Storage); // Pseudo-code illustrating calling constructor
    loadResource(&Storage, <args>); // considering loadResource can't be inlined
    Storage.~Storage();
    

    In code above, compiler will see that both constructor and destructor are default, and since class has no member are, indeed, trivial - so those could be safely omitted.

    What you end up with is a necessity to allocate 1 byte in automatic storage, which on modern architectures usually means decrementing stack pointer register, following by incrementing it.

    This is incredibly fast operation, but it still not instantaneous.