1

I am trying to implement a generic interface using the PIMPL idiom that requires some template member functions to provide functionality for ANY type. The issue is I can't find any way to support using template functions with the PIMPL idiom without pre-declaring every possible type that the function might use.

See related question: Pimpl Idiom with template member function

The accepted answer to that question suggests explicitly defining every type that will be used with the template. This is not always practical. Consider the following example:

example.h

#ifndef EXAMPLE_H
#define EXAMPLE_H

#include <memory>

class Example
{
public:
    Example();
    ~Example();

    // Generic template function in the interface
    template <typename T>
    void PrintData(const T& data) const;

private:
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

#endif

example.cpp

#include "example.h"
#include <iostream>

// Private implementation
struct Example::Impl
{
    template <typename T>
    void PrintData(const T& data) const
    {
        std::cout << data << std::endl;
    }
};

// Note: using C++11 so "make_unique" is not available
Example::Example() : m_impl(new Impl()) {}
Example::~Example() = default;

// Forward the method call to the implementation
template <typename T>
void Example::PrintData(const T& data) const
{
    m_impl->PrintData(data);
}

// Don't want to have to explicitly state all types...
// this is not practical because "PrintData" should work for ANY printable type.

// Uncommenting the line below will allow this example to compile,
// but it will only compile when used with "int" data.
//template void Example::PrintData(const int& data) const;

main.cpp

#include "example.h"

int main()
{
    Example ex;
    ex.PrintData(42);
}

Attempting to compile this will fail with an undefined reference error:

g++ -std=c++11 -o main main.cpp example.cpp
/tmp/cc6IhZsx.o: In function `main':
main.cpp:(.text+0x2b): undefined reference to `void Example::PrintData<int>(int const&) const'
collect2: error: ld returned 1 exit status

This example provides a very generic function in the interface that should work on ANY printable type. This poses an issue because we can't feasibly predict all types this function will be used on until compile-time.

How can I get a function like this to work with the PIMPL idiom (or some other method of creating a "compiler firewall")

Note that this example is intentionally trivial to demonstrate the problem. In the real world I have classes that contain MUCH more complicated template functions in the interface that need to work on many types. Because of this I have been unable to get these classes behind a compiler firewall like the PIMPL idiom provides.

I am open to considering alternatives to the PIMPL idiom that can achieve the desired effect if any such thing exists.

tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
  • Are you sure that you really want _Pimpl_ rather than _CRTP_? – πάντα ῥεῖ Jun 07 '19 at 18:06
  • If you have to use the PIMPL idiom, the best you can do is some kind of type erasure. Perhaps `std::function` or the proposed `std::function_ref` will let you do what you need. – Justin Jun 07 '19 at 18:07
  • I will look into both CRTP and type erasure, I'm not particularly familiar with both. – tjwrona1992 Jun 07 '19 at 18:08
  • What do you really want to hide with pimpl of the template? – Jarod42 Jun 07 '19 at 18:31
  • @Jarod42 preferably the entire implementation of the templated function, although from what I understand that may not be possible due to limitations of the language. – tjwrona1992 Jun 07 '19 at 18:37
  • If you really want *any* type, then [std::any](https://en.cppreference.com/w/cpp/utility/any) seems relevant. – Jesper Juhl Jun 07 '19 at 18:51
  • 1
    @JesperJuhl you would still need to know the type inside of the implementation before hand in order to know how to convert it back to its original type to print it. – tjwrona1992 Jun 07 '19 at 19:06

1 Answers1

0

If you must use the PImpl idiom and you must support any type, the best you can do is to type erase the behavior. This allows you to hide part of the implementation, but not all of it.

For simple cases, std::function or the proposed std::function_ref can work:

#include <memory>
#include <ostream> // We must define some of the behavior in the header file.
#include <functional> // for std::function

class Example
{
public:
    Example();
    ~Example();

    // Generic template function in the interface
    template <typename T>
    void PrintData(const T& data) const;

private:
    struct Impl;
    std::unique_ptr<Impl> m_impl;

    // Some alternatives:
    // Some cost:
    void PrintDataImpl(const void* obj,
         const std::function<void(std::ostream&, const void*)>&) const;
    // Not yet standardized:
    void PrintDataImpl(const void* obj,
         std::function_ref<void(sstd::ostream&, const void*)>) const;
    // C-style:
    void PrintDataImpl(const void* obj,
         void(*fn)(std::ostream&, const void*)) const;
};

template <typename T>
void Example::PrintData(const T& data) const
{
    // std::function is expensive, but in this case, we fit in the small buffer
    // optimization as a lambda with no captures.
    // If we used `std::function_ref`, this would be cheaper.
    // If we used the C-style, the lambda cannot capture anything.
    PrintDataImpl(&data, [](std::ostream& out, const void* obj) {
        out << *static_cast<const T*>(obj);
    });
}


// In Example.cpp:
void Example::PrintDataImpl(const void* obj,
     const std::function<void(std::ostream&, const void*)>& fn) const
{
    // Call the function to apply the one piece of type-erased behavior.
    fn(std::cout, obj);
    std::cout << std::endl;
}
tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
Justin
  • 24,288
  • 12
  • 92
  • 142
  • But you don't hide anything in pimpl then... (`` has be include in header for example). – Jarod42 Jun 07 '19 at 18:24
  • It does appear as though the implementation is now exposed and not hidden in the PIMPL anymore if you do this. If that's not the case can you please explain? I don't think I fully understand. – tjwrona1992 Jun 07 '19 at 18:27
  • @Jarod42 Exactly. This only makes sense for more complicated cases where you have something else that you want to hide – Justin Jun 07 '19 at 18:32
  • 1
    Ahh I see, so it basically extracts out the templated portion and exposes it, but keeps anything that isn't templated hidden away. That's an interesting technique. – tjwrona1992 Jun 07 '19 at 18:35