问题
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?
- How is
fn2
an incomplete type? - What changes are necessary to the signature of
fn2
, to allow me to assign it a type offn1
? 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 tovoid*
float
arguments are converted todouble
as in floating-point promotionbool
,char
,short
, and unscoped enumerations are converted toint
or wider integer types as in integer promotionOnly 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