Implementing pImpl based wrapper around a class using variadic template functions
Asked Answered
D

1

6

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.

Doubleteam answered 9/6, 2019 at 18:13 Comment(6)
You have the definition of Logger::info in a .cpp file. Are you aware that all template definitions needs to be available at compile time? That's why you normally put them in the header. That includes the ones in your third party dependencies.Saran
pImpl and templates don't mix, pimpl hides all implementation details, templates generate code at the users side based on the template arguments and want to know the implementation. I would use c++20 format library to convert everything to a single string and than forward to the implementation.Porscheporsena
Thank you for the responses. @ super, I am now. I've read a lot about templates this last couple days. @Porscheporsena That formatting library isn't yet officially part of the C++ standard, right? Also, I don't see how using fmtlib would help me. It's already in spdlog, plus it's also built using variadic templates, so wouldn't it present me with the problem I'm already having? ... Is the consensus that there is no way to hide third-party libraries from clients when they use templates? Will I have no choice but to either expose the extra dependency to my clients, or roll out my own solution? :-(Doubleteam
It is officially voted into C++20, which ain't officially released, it I'm correct. (Maybe they still needed to do the wording) There is a github repo that contains a fully working version by the author of the proposalPorscheporsena
Since format uses a similar syntax, or even the same one, you could, via variadic and format convert it to a single argument (string), which you could forward to your impl.Porscheporsena
I don't understand. How would eliminate my problem? Even if I tried to create a wrapper around fmtlib I'd be exposing it to my library's clients just as I am with spdlog. My wrapper would need to be made with variadics which would just expand to their variadics, thus the client dependency. I'm trying to "do things right" by isolating the libraries I'm using from the clients of the library I'm making, but from everything I've been studying over this last weekend that's looking more and more impossible due to the nature of variadic templates. fmtlib and spdlog seem to be unhideable. My options?Doubleteam
S
3

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)});
}
Sterol answered 12/6, 2019 at 9:56 Comment(2)
Thank you for this. I really appreciate the detailed examples and learned a lot from taking them apart. Sadly, this doesn't side step spdlog's use of variadic template functions, though it would work perfectly if that library provided even one list-based logging procedure.Doubleteam
@Doubleteam you could provide a fmt custom formatter for erased_type struct erased_type : std::vector<std::any>{};Sterol

© 2022 - 2024 — McMap. All rights reserved.