Strange behavior with std::function

2019-05-03 11:54发布

问题:

I'm using the standard function wrapper from the C++11 library, and I am seeing some strange behavior with its boolean operator. If I create a std::function object the boolean operator returns false. This is still true if I assign nullptr to the object and check again. The problem appears when I assign it a void pointer which I have cast into a function pointer. Consider the following program:

#include <functional>
#include <iostream>

void* Test() {
    return nullptr;
}

int main(int argc, char* argv[]) {
    std::function<void()> foo;
    std::cout << !!foo << std::endl;

    foo = nullptr;
    std::cout << !!foo << std::endl;

    foo = reinterpret_cast<void(*)()>(Test());
    std::cout << !!foo << std::endl;

    return 0;
}

What I expect as output is 0 0 0 but the result is 0 0 1 (see demo). Can anyone explain why the boolean operator returns true when it contains a null, non-callable function pointer? And please also mention a workaround to check for nullptr in std::function

NOTE: I've already tried checking if the target is null (with foo.target<void*>() == nullptr) instead of using the boolean operator, but it seems as if no matter what the function object contains, the target is always null (even when the function object is perfectly fine with being called).

回答1:

Looks like a bug to me. First, here's a simplified example that doesn't play any games with casts:

#include <functional>
#include <iostream>

typedef void (*VF)();

VF Test() {
    return nullptr;
}

int main(int argc, char* argv[]) {
    std::function<void()> foo(Test());
    std::cout << !!foo << std::endl;
    return 0;
}

It still prints 1 with GCC. It shouldn't:

20.8.11.2.1
template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);
7 Requires: F shall be CopyConstructible. f shall be Callable (20.8.11.2) for argument types ArgTypes and return type R. The copy constructor and destructor of A shall not throw exceptions.
8 Postconditions: !*this if any of the following hold:

  • f is a NULL function pointer.
  • f is a NULL pointer to member.
  • F is an instance of the function class template, and !f


回答2:

I don't think the code is doing what you think it does. This line:

foo = reinterpret_cast<void(*)()>(Test());

means that you receive a void* from Test(). You then procede to reinterpret_cast this pointer-to-object into a pointer-to-function. This is not allowed and therefore the code yields undefined behavior and therefore any output of the compiler is valid.

The relevant parts of the standard are

5.2.10 Reinterpret cast [expr.reinterpret.cast]

8 Converting a function pointer to an object pointer type or vice versa is conditionally-supported. The meaning of such a conversion is implementation-defined, except that if an implementation supports conversions in both directions, converting a prvalue of one type to the other type and back, possibly with dierent cv- qualification, shall yield the original pointer value.

9 The null pointer value (4.10) is converted to the null pointer value of the destination type. [Note: A null pointer constant of type std::nullptr_t cannot be converted to a pointer type, and a null pointer constant of integral type is not necessarily converted to a null pointer value. — end note ]

and

4.10 Pointer conversions [conv.ptr]

1 A null pointer constant is an integral constant expression (5.19) prvalue of integer type that evaluates to zero or a prvalue of type std::nullptr_t. A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type and is distinguishable from every other value of object pointer or function pointer type.

(emphasis mine)

Here's a reduced test case taking std::function (and its possible bugs) out of the equation:

#include <iostream>

int main() {
    using fp_t = void(*)();
    void* vn = nullptr;
    fp_t foo = reinterpret_cast<fp_t>(vn); // GCC: warning, Clang: silence
    //fp_t foo = reinterpret_cast<fp_t>(nullptr); // error (GCC and Clang!)
    std::cout << !!foo << std::endl;
}

Live example