Implementing pImpl based wrapper around a class using variadic template functions

Viewed 1327

Summary

I'm writing a library and a client application. In the library I'm trying to write a wrapper around another statically linked third-party library (specifically, spdlog) and am trying to use the pImpl idiom to completely hide it from the client application. The problem is that the third-party library uses variadic template functions, and so I need to in my library too.

Background

My first attempt at a wrapper was pretty thin and straight forward, but then I was getting "No such file or directory" errors in my client application because the third-party header was being included in my library's header.

I next tried to create a pImpl class and got that to compile, but again in the client I was getting "undefined reference" linker errors.

Pulling the source code for the implementation into the header for my wrapper puts me right back at the initial "No such file" problem. After researching this I'm starting to think making a wrapper around variadic templates isn't possible, but I'm not sure. This is the first time I've ever tried to make a variadic function/template.

Example Code

Here's how my project currently stands:

Almost all namespaces, function names, headers, etc. have all been edited (or removed) for brevity and clarity.

Client Application - sandbox.cpp

#include "sandbox.h"
#include <logger.h>  //  <-- This is all I want clients to see.

int Sandbox::run() {
    LOG_INFO("Hello World!");        // My library is providing this.
    LOG_INFO("Hello {}", "indeed!"); // And, this variable input function.
    return 0;
}

My Library - logger.h

class LoggerImp;  // Forward declaration of implementation.

class LIB_EXPORT Logger {
  public:

    /* Constructors, destructor, etc. */

    template <typename... Args>
    void info(const char * fmt, Args &... args);

    void info(const char * msg) { this->info("{}", msg); }

    /* Other logging functions: trace, error, etc. */

  private:
    LoggerImp * _imp;
};

static Logger coreLogger("Core");
static Logger clientLogger("App");

#define LOG_INFO(args...) clientLogger.info(args)
/* Other such convenience definitions. */

My Library - logger.cpp

#include "logger.h"
#include "loggerimp.h"

Logger::Logger(std::string name) { _imp = new LoggerImp(name, this); }
Logger::~Logger() { delete _imp; }

template <typename... Args>
void Logger::info(const char * fmt, Args &... args) {
    _imp->info(fmt, args...);
}

My Library - loggerimp.h

#include "logger.h"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>

class LoggerImp {
  public:
    explicit LoggerImp(string name, Logger * pubInterface) :
        _pubInterface(pubInterface) {  // Back pointer.
        _sink   = make_shared<spdlog::sinks::stdout_color_sink_mt>();
        _logger = make_shared<spdlog::logger>(name, _sink);
        spdlog::initialize_logger(_logger);
        // The above three lines create the actual logging object
        // that my library is wrapping and hiding from its clients.
    }

    template <typename... Args>
    inline void info(const char * fmt, const Args &... args) {
        _logger->info(fmt, args...);  // Third-party logging function.
    }
}

Expected Results

As mentioned above I just want clients of my library to be able to include headers like <logger.h> and not need to configure their projects to find and handle all of my library's dependencies as well, but since I'm currently using a third-party tool that uses variadic templates I'm not seeing any way that I can hide that from my clients given the, um... "not a real function" nature of templates.

1 Answers

You can do type-erasure in header file, and deal with the type-erased type in impl source file.

Here's two example:

1. type-erasure using std::any

// log.hpp
#pragma once

#include <any>
#include <vector>
#include <utility>

struct Log {
  Log(int a);
  ~Log();

  template <class... A>
  void log(A&&... a) {
    log_impl({std::any(std::forward<A>(a))...});
  }

private:
  void log_impl(std::vector<std::any> v);
  struct Impl;
  Impl* impl_;
};

// log.cpp
#include "log.hpp"
#include <iostream>
#include <boost/mp11.hpp>

struct Log::Impl {
  int a;
};

void Log::log_impl(std::vector<std::any> v) {
  std::cout << impl_->a << " ";
  for (auto&& i : v) {
    bool b = false;
    using namespace boost::mp11;
    mp_for_each<mp_list<int, const char*, double>>(
        [&](auto t) {
          if (!b) {
            try {
              std::cout << std::any_cast<decltype(t)>(i) << " ";
              b = true;
            } catch (std::bad_any_cast&) {
            }
          }
        });
    if (!b) {
      std::cout << "UNKNOWN ";
    }
  }
  std::cout << std::endl;
}

Log::Log(int a) : impl_(new Log::Impl{a}) {}
Log::~Log() { delete impl_; }


// client.cpp
#include "log.hpp"

struct A {
  char a;
};
std::ostream& operator<<(std::ostream& os, const A& a) { os << a.a; }

int main() {
  Log a(555);
  a.log(11, "222");    // output: 555 11 222 
  a.log(A{'a'}, 3.3);  // output: 555 UNKNOWN 3.3 
}

2. type-erasure using std::function

// log.hpp
#pragma once

#include <vector>
#include <utility>
#include <functional>
#include <iostream>

struct Log {
  Log(int a);
  ~Log();

  template <class... A>
  void log(A&&... a) {
    log_impl({[&a](std::ostream& os) { os << std::forward<A>(a); }...});
  }

private:
  void log_impl(std::vector<std::function<void(std::ostream&)>> v);
  struct Impl;
  Impl* impl_;
};


// log.cpp
#include "log.hpp"
#include <iostream>

struct Log::Impl {
  int a;
};

void Log::log_impl(std::vector<std::function<void(std::ostream&)>> v) {
  std::cout << impl_->a;
  for (auto&& i : v) {
    std::cout << ' ';
    i(std::cout);
  }
  std::cout << std::endl;
}

Log::Log(int a) : impl_(new Log::Impl{a}) {}
Log::~Log() { delete impl_; }


// client.cpp
#include "log.hpp"

struct A {
  char a;
};
std::ostream& operator<<(std::ostream& os, const A& a) { os << a.a; }

int main() {
  Log a(555);
  a.log(11, "222");    // output: 555 11 222
  a.log(A{'a'}, 3.3);  // output: 555 a 3.3
}

Provide fmt::formatter for type-erased type

here's an example on providing fmt custom formatter for the type-erased type.

namespace {
struct erased_type : std::vector<std::any> {};
} // namespace

namespace fmt {
template <>
struct formatter<erased_type> {
  template <typename ParseContext>
  constexpr auto parse(ParseContext &ctx) { return ctx.begin(); }

  template <typename FormatContext>
  auto format(const erased_type &v, FormatContext &ctx) {
    auto ctx_itr = ctx.begin();
    for (auto&& i : v) {
      bool b = false;
      using namespace boost::mp11;
      mp_for_each<mp_list<int, const char*, double>>([&](auto t) {
        if (!b) {
          try {
            format_to(ctx_itr, " {}", std::any_cast<decltype(t)>(i));
            b = true;
            ctx_itr++;
          } catch (std::bad_any_cast&) {
          }
        }
      });
      if (!b) {
        format_to(ctx_itr++, " UNKNOWN");
      }
    }
    return ctx_itr;
  }
};
}

void Log::log_impl(std::vector<std::any> v) {
  spdlog::info("{} {}", impl_->a, erased_type{std::move(v)});
}
Related