What a profound and thought provoking question! Nice one OP!
The basic idea behind this solution is using macros to generate class template specializations that can tell you the name of object types, using template programming to compose those class template specializations into specializations for function pointers, and using function overloading to search among the specializations.
type_string.h
#ifndef TYPE_STRING_H
#define TYPE_STRING_H
#include <type_traits>
#include <cstdlib>
#include <cstring>
#include <string>
// is_fun_ptr
// Dietmar Kühl
// http://stackoverflow.com/a/18667268/1128289
template <typename Fun>
struct is_fun_ptr
: std::integral_constant<bool, std::is_pointer<Fun>::value
&& std::is_function<
typename std::remove_pointer<Fun>::type
>::value>
{
};
template<typename T>
struct TypeString;
// type_string<>() overload for non-function pointer objects
// caller must free
template<typename T>
typename std::enable_if<!is_fun_ptr<T>::value, const std::string>::type
type_string()
{
const std::string name = TypeString<T>::value();
char* name_c_str = (char*)malloc(name.length() + 1);
strcpy(name_c_str, name.c_str());
return name_c_str;
}
// getting type name into a template
// atzz and Steve Jessop
// http://stackoverflow.com/a/4485051/1128289
// There's likely a better way to handle CV qualifiers and pointer/ref
// not all combos covered here
#define ENABLE_TYPE_STRING(TYPE) \
template<> \
struct TypeString<TYPE> { \
static const std::string value() { return #TYPE; } \
}; \
template<> \
struct TypeString<TYPE&> { \
static const std::string value() { return #TYPE "&"; } \
}; \
template<> \
struct TypeString<TYPE*> { \
static const std::string value() { return #TYPE "*"; } \
}; \
template<> \
struct TypeString<const TYPE> { \
static const std::string value() { return "const " #TYPE; } \
}; \
template<> \
struct TypeString<const TYPE&> { \
static const std::string value() { return "const " #TYPE "&"; } \
}; \
template<> \
struct TypeString<const TYPE*> { \
static const std::string value() { return "const " #TYPE "*"; } \
}; \
template<> \
struct TypeString<const TYPE* const> { \
static const std::string value() { return "const " #TYPE "* const"; } \
};
// some builtin types, add others and user-defined types as desired
ENABLE_TYPE_STRING(char)
ENABLE_TYPE_STRING(short)
ENABLE_TYPE_STRING(int)
ENABLE_TYPE_STRING(long)
ENABLE_TYPE_STRING(long long)
ENABLE_TYPE_STRING(signed char)
ENABLE_TYPE_STRING(unsigned char)
ENABLE_TYPE_STRING(unsigned short)
ENABLE_TYPE_STRING(unsigned int)
ENABLE_TYPE_STRING(unsigned long)
ENABLE_TYPE_STRING(unsigned long long)
ENABLE_TYPE_STRING(float)
ENABLE_TYPE_STRING(double)
ENABLE_TYPE_STRING(long double)
ENABLE_TYPE_STRING(std::string)
// void is a special case, no qualifiers, refs
template<>
struct TypeString<void>
{
static const std::string value()
{
return "void";
}
};
template<>
struct TypeString<void*>
{
static const std::string value()
{
return "void*";
}
};
// Function signature to string
// return_type
// angew
// http://stackoverflow.com/a/18695701/1128289
template <class F>
struct return_type;
template <class R, class... A>
struct return_type<R (*)(A...)>
{
typedef R type;
};
// forward declaration so that this overload may be used in CommaSeparatedNames
template<typename T>
typename std::enable_if<is_fun_ptr<T>::value, const std::string>::type
type_string();
// Concatenating argument types with separating commas
template<typename T, typename... Us>
struct CommaSeparatedNames
{
static const std::string value()
{
return std::string{type_string<T>()}
+ ", " + CommaSeparatedNames<Us...>::value();
}
};
template<typename T>
struct CommaSeparatedNames<T>
{
static const std::string value()
{
return type_string<T>();
}
};
// Arguments to string
template <class F>
struct ArgNames;
template<class R>
struct ArgNames<R (*)()>
{
static const std::string value()
{
return "";
}
};
template<class R, typename A, typename... As>
struct ArgNames<R (*)(A, As...)>
{
static const std::string value()
{
return CommaSeparatedNames<A, As...>::value();
}
};
// overload type_string<>() for function pointers
// caller must free
template<typename T>
typename std::enable_if<is_fun_ptr<T>::value, const std::string>::type
type_string()
{
const auto name =
std::string{type_string<typename return_type<T>::type>()}
+ " (*)(" + ArgNames<T>::value() + ")";
auto name_c_str = (char*)malloc(name.length() + 1);
strcpy(name_c_str, name.c_str());
return name_c_str;
}
// overload type_string<>() to deduce type from an object
// caller must free
template<typename T>
const std::string type_string(T) { return type_string<T>(); }
template<typename T>
const char* type_string_c_str()
{
const auto name = type_string<T>();
char* name_c_str = (char*)malloc(name.length() + 1);
strcpy(name_c_str, name.c_str());
return name_c_str;
}
template<typename T>
const char* type_string_c_str(T) { return type_string_c_str<T>(); }
#endif
Example usage:
#include "type_string.h"
#include <iostream>
void foo() {}
int bar() { return 0; }
float baz(char) { return 0.0f; }
class MyClass;
ENABLE_TYPE_STRING(MyClass)
double quux(const int*, MyClass*) { return 0.0; }
int main()
{
typedef void (*FooTypePtr)();
typedef int (*BarTypePtr)();
typedef float (*BazTypePtr)(char);
typedef double (*QuuxTypePtr)(const int*, MyClass*);
FooTypePtr foo_ptr = foo;
BarTypePtr bar_ptr = bar;
BazTypePtr baz_ptr = baz;
QuuxTypePtr quux_ptr = quux;
QuuxTypePtr (*weird_ptr)(FooTypePtr, BarTypePtr, BazTypePtr) = NULL;
std::cout << type_string(3) << std::endl;
std::cout << type_string('a') << std::endl;
std::cout << type_string(foo_ptr) << std::endl;
std::cout << type_string(bar_ptr) << std::endl;
std::cout << type_string(baz_ptr) << std::endl;
std::cout << type_string(quux_ptr) << std::endl;
std::cout << type_string(weird_ptr) << std::endl;
}
Output:
int
char
void (*)()
int (*)()
float (*)(char)
double (*)(const int*, MyClass*)
double (*)(const int*, MyClass*) (*)(void (*)(), int (*)(), float (*)(char))
Type information for object types needs to be manually enabled but types for function pointers can be deduced if info for all the involved types was enabled.
Dynamic loading example (aka using X to solve Y)
Here's a way to detect such type errors at runtime.
safe_dl.h
uses type_string.h
to error check dynamic loading.
#ifndef SAFE_DL_H
#define SAFE_DL_H
#include "type_string.h"
#include <exception>
#include <stdexcept>
#include <sstream>
#include <string>
#include <dlfcn.h>
#define ENABLE_RUNTIME_FNC_PTR_TYPE_INFO(F) \
extern "C" const char* F ## _fnc_ptr_type_() { \
return type_string_c_str( F ); }
#define RUNTIME_TYPE_STRING_FNC_NAME(F) \
F ## _fnc_ptr_type_
namespace {
extern "C" void in_dll_free(void* ptr)
{
free(ptr);
}
}
class DynamicLibrary
{
public:
explicit DynamicLibrary(const char* lib_name, const int mode=RTLD_LAZY)
: filename_(lib_name)
{
handle_ = dlopen(lib_name, mode);
if (handle_ == NULL) {
throw std::runtime_error(dlerror());
}
free_ptr_ = find_in_dll_free();
}
DynamicLibrary(const std::string& lib_name)
: DynamicLibrary(lib_name.c_str()) {}
~DynamicLibrary()
{
dlclose(handle_);
}
template<typename T>
T safe_dynamic_load(const std::string& func_name) const
{
// The type of T* tells us the expected type. A cooperating library tells
// us the actual type.
const auto expected_type = type_string<T>();
const auto actual_type = symbol_type(func_name);
if (strcmp(expected_type.c_str(), actual_type))
{
std::ostringstream msg;
msg << "Function pointer type mismatch. Function " << func_name
<< " loaded from file " << filename() << " has expected type "
<< expected_type << " but the actual type is " << actual_type
<< ".";
free_ptr_((void*)actual_type);
throw std::runtime_error(msg.str());
}
free_ptr_((void*)actual_type);
return (T)(symbol(func_name));
}
const std::string& filename() const { return filename_; }
private:
// caller is responsible for freeing returned pointer
const char* symbol_type(const std::string& name) const
{
const auto type_func_name = name + "_fnc_ptr_type_";
typedef const char* (*TypeFuncPtrType)();
TypeFuncPtrType type_func_ptr =
(TypeFuncPtrType)dlsym(handle_, type_func_name.c_str());
if (type_func_ptr == NULL) {
const auto msg = "Safe dynamic loading not enabled for " + name;
throw std::runtime_error(msg.c_str());
}
return type_func_ptr();
}
void* symbol(const std::string& name) const
{
void* p = dlsym(handle_, name.c_str());
if (p == NULL) {
throw(std::runtime_error{dlerror()});
}
return p;
}
// free from within the dll
typedef void (*DllFreePtrType)(void*);
DllFreePtrType find_in_dll_free() const
{
typedef void (*DllFreePtrType)(void*);
DllFreePtrType free_ptr = (DllFreePtrType)dlsym(handle_, "in_dll_free");
if (free_ptr == NULL) {
throw std::runtime_error(dlerror());
}
return free_ptr;
}
private:
const std::string filename_;
void* handle_;
DllFreePtrType free_ptr_;
};
#endif
Work safe_dl.h
into the original program:
main.cpp
#include "safe_dl.h"
#include <iostream>
#if defined(__clang__)
#define COMPILER "clang"
#elif defined(__GNUC__)
#define COMPILER "gcc"
#else
#define COMPILER "other"
#endif
typedef void (*FooPtrType)();
typedef int (*BarPtrType)();
int main()
{
std::cout << "main()" << std::endl;
std::cout << "compiler: " COMPILER << std::endl;
const DynamicLibrary dll{"./functions.so"};
// Works fine.
const auto foo_ptr = dll.safe_dynamic_load<FooPtrType>("foo");
foo_ptr();
// Throws exception.
const auto bar_ptr = dll.safe_dynamic_load<FooPtrType>("bar");
bar_ptr();
}
functions.cpp
, compiled to functions.dylib
#include "safe_dl.h"
#include <iostream>
#if defined(__clang__)
#define COMPILER "clang"
#elif defined(__GNUC__)
#define COMPILER "gcc"
#else
#define COMPILER "other"
#endif
extern "C" void foo() {
std::cout << "foo()" << std::endl;
std::cout << "compiler: " COMPILER << std::endl;
return;
}
ENABLE_RUNTIME_FNC_PTR_TYPE_INFO(foo)
extern "C" int bar() {
std::cout << "bar()" << std::endl;
return 0;
}
ENABLE_RUNTIME_FNC_PTR_TYPE_INFO(bar)
Output for main.cpp
compiled with clang and functions.cpp
compiled with g++:
main()
compiler: clang
foo()
compiler: gcc
terminate called after throwing an instance of 'std::runtime_error'
what(): Function pointer type mismatch. Function bar loaded from file
./functions.so has expected type void (*)() but the actual type is int (*)().
Aborted (core dumped)
Because type_name.h
generates char *
to encode types the code is relatively compiler agnostic and safe_dl.h
will work with mixed compilers. (Or at least the example works when mixing gcc 4.9.2 and clang 3.5.)