How to translate `void(*fn)(const char *, …)` to `std::function` and vice versa

半世苍凉 提交于 2020-01-03 15:42:40

问题


typedef void(*fn1)(const char *, ...);
typedef std::function<void(const char *, ...)> fn2; // has initializer but incomplete type

Intuitively, these are the effectively the same to me, but obviously my intuition is failing me. How would I reconcile these data types?

  1. How is fn2 an incomplete type?
  2. What changes are necessary to the signature of fn2, to allow me to assign it a type of fn1?
  3. When creating a lambda to assign to fn2, how do I access the variadic argument list?

    In other words, what is the lambda equivalent to the following?

    void fn1_compatible (const char * format, ...) {
      va_list args;
      va_start(args, format);
      //TODO: Do stuff with variadic arguments
      va_end(args);
    }
    

NOTE: As an aside, these signatures are related to logging, but please answer the question in a general (not logging) context.


回答1:


Variadic functions are not supported by std::function. std::function takes one type, and it looks something like this:

template<class>
class function;  // Intentionally incomplete

template<class Ret, class... Args>
class function<Ret(Args...)> {
    // Deduce return type and argument types from function type
};

But this does not deduce the types for variadic function. So void(const char*) would work (Ret is void and Args... is const char*), but void(const char*, ...) would not work (As that would need to be deduced from Ret(Args..., ...))

To make a functor object out of it, either just use a bare function pointer, like you did with fn1, or make do what the C standard library does with functions like vprintf:

decltype(auto) my_variadic_function(const char * format, ...) {
  va_list args;
  va_start(args, format);
  try {
      auto&& return_value = vmy_variadic_function(format, args);
  } catch (...) {
      va_end(args);
      throw;
  }
  va_end(args);
  return std::forward<decltype(return_value)>(return_value);
}

void vmy_variadic_function(const char* format, va_list args) {
    // Do stuff with args
}

And then pass vmy_variadic_function in a std::function<void(const char*, va_list)>.




回答2:


As far as I know, you cannot.

If you can alter your premise a bit. That is; instead of using for example printf, you could use vprintf. Then you could have:

using fn2 = std::function<int(const char*, va_list)>;
fn2 fun = vprintf;

You can then provide a wrapper function to call a fn2 with ... arguments:

int fun_wrapper(const char *format, ...) {
    va_list args;
    va_start(args, format);
    int ret = fun(format, args);
    va_end(args);
    return ret;
}

Usually ... functions are just wrappers for the va_list alternative that contains the actual implementation. Just like the shown fun_wrapper is a wrapper for fun.

But if the ... function that you want to use doesn't have va_list list version, and isn't implemented by you, then your best alternative might be to use something else.




回答3:


As others have pointed out, there's no way to map a C-style variadic list of arguments to a strongly-typed C++ function. It's possible to go the other way though, and it's possible to do this really safely.

Here, I've written a function forward_to_variadic_fn that accepts a C-style variadic function and a list of strongly-typed arguments. Variadic arguments have lots of restrictions about correct usage, so I decided to implement some safety checks that enforce these restrictions at compile time. For example, using this forwarding function, you can't accidentally pass a std::string when you should be passing a const char*

// true if T is trivially copy & move constructible and trivially destructible
template<typename T>
constexpr bool trivial_class = (std::is_trivially_copy_constructible_v<T> && std::is_trivially_move_constructible_v<T> && std::is_trivially_destructible_v<T>);

// true if T is acceptable for C-style va_args
template<typename T>
constexpr bool safe_for_va_args = (std::is_null_pointer_v<T> || std::is_pointer_v<T> || std::is_arithmetic_v<T> || std::is_member_pointer_v<T> || trivial_class<T>);

// true if all of Args.. are safe for C-style va_args
template<typename... Args>
constexpr bool all_safe_for_va_args = (true && ... && safe_for_va_args<std::decay_t<Args>>);

template<typename Ret, typename... Args>
Ret forward_to_variadic_fn(Ret(*the_fn)(const char*, ...), const char* format, Args... args){
    static_assert(all_safe_for_va_args<Args...>, "The provided types are not safe for use with C-style variadic functions.");
    return the_fn(format, args...);
}

int main(){

    int n = forward_to_variadic_fn(std::printf, "Hello, %s!\n", "world");
    std::cout << n << " characters were written.\n";

    std::string mystr = "world!";

    // This will compile but is BAD
    // std::printf("Hello, %s!\n", mystr);

    // The following line will not compile, which is a good thing!
    // forward_to_variadic_fn(std::printf, "Hello, %s!\n", mystr);

    // This is safe
    n = forward_to_variadic_fn(std::printf, "Hello, %s!\n", mystr.c_str());
    std::cout << n << " characters were written.\n";

    return 0;
}

Live example here

Of course, this can't save you from using incorrect formatting flags, but it could save you from lots of other undefined behavior.

Edit for explanation: the helper template variable all_safe_for_va_args serves to enforce the restrictions on arguments to variadic functions as explained on cppreference:

When a variadic function is called, after lvalue-to-rvalue, array-to-pointer, and function-to-pointer conversions, each argument that is a part of the variable argument list undergoes additional conversions known as default argument promotions:

  • std::nullptr_t is converted to void*
  • float arguments are converted to double as in floating-point promotion
  • bool, char, short, and unscoped enumerations are converted to int or wider integer types as in integer promotion

Only arithmetic, enumeration, pointer, pointer to member, and class type arguments are allowed (except class types with non-trivial copy constructor, non-trivial move constructor, or a non-trivial destructor, which are conditionally-supported with implementation-defined semantics)

These individual conditions are neatly captured by the many helper traits classes and helper template variables in the <type_traits> library. For example, std::is_null_pointer_v<T> is a compile-time constant that evaluates to true if and only if T is nullptr_t.

The variable template safe_for_va_args<T> checks these requirements exhaustively and thus is true whenever the type T satisfies the above conditions. To make a variable template for this same condition that accepts any number of types, I used a fold expression to efficiently do a logical AND over a parameter pack expansion, which can be seen in the implementation of all_safe_for_va_args<T>.

For example, all_safe_for_va_args<int, nullptr, const char*> evaluates to:

(true && safe_for_va_args<int> && safe_for_va_args<nullptr> && safe_for_va_args<const char*>)

all of which are true, so the whole expression is true.

This combines nicely with a static_assert, which checks a custom condition at compile time, and can provide a user-friendly error message rather than a cryptic chain of template substitution failures.

In general, the types you're allowed to pass to a C-style variadic function are exclusively types that can be copied bit-for-bit and don't need any special maintenance. std::string fails this because it must perform memory allocations and deallocations, and so has neither a trivial constructor nor destructor. A const char* or int on the other hand can safely be copied bit-for-bit and considered logically identical.




回答4:


The lambda form for varargs is a straightforward conversion of the standalone function form.

auto thing = [](const char * format, ...) {
  va_list args;
  va_start(args, format);
  //TODO: Do stuff with variadic arguments
  va_end(args);
};

This is accepted by G++.

Unfortunately clang seems to have a bug where it doesn't recognize that its __builtin__vastart keyword is being used inside a lambda with variable argument list.



来源:https://stackoverflow.com/questions/55423225/how-to-translate-voidfnconst-char-to-stdfunction-and-vice-vers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!