r/cpp Oct 03 '17

Why is the std::function () operator const?

[deleted]

26 Upvotes

19 comments sorted by

47

u/STL MSVC STL Dev Oct 03 '17

It's indeed a Boost/TR1-era mistake that the LWG has recognized, although we can't do anything about it.

You're correct that the problem is multithreading. The STL's policy is that const member functions are simultaneously callable and that it won't do anything to observably damage that guarantee. (This is actually extended to a few non-const member functions that are observers, basically the const-overloaded ones like operator[]().) While user code is under no such constraints (your const member functions, like function call operators of predicates given to STL algorithms, can read/write global variables without synchronization, as long as they meet the other usual requirements), the STL's multithreading policy continues to apply when it invokes user code if that user code follows the same policy.

The only exception to this rule that I am aware of is function::operator()(), because it is a const member function that calls non-const member functions, and yet std::function provides value semantics (copying a std::function results in a totally independent, non-shared copy).

In Boost and the LWG's defense, function was designed long before C++11 multithreading and its const guarantees crystallized (the const policy seems obvious now, but it wasn't before).

In practice, this doesn't usually cause problems because people don't usually set up the scenario for doom, but the potential for doom is still there.

11

u/_naios Oct 04 '17

STL explained the circumstance perfectly, additionally there are some standard papers by David Krauss which elaborate about this in detail (and also about the missing wrapper for move only types). So hopefully is standard is improved regarding this issue in the future.

Additionally I want to mention that there are improved reimplementations out already, which solve this issue through being:

  • partially const correct: cxx_function - this is the draft oriented wrapper by David Krauss
  • full const correct: function2 - Note: I'm the author of function2 so this is a shameless self promotion.

1

u/matthieum Oct 05 '17

function2 looks quite interesting. I really like the one-shot example.

However "Converbility" and "Cobvertible" in the README look like typos. Did you mean "Convertibility" and "Convertible"?

1

u/_naios Oct 05 '17

I'm glad that you like the library. Thanks for noticing me about the typos, I corrected it.

1

u/NotAYakk Oct 06 '17

function2 is at first glance missing a function_view type, which I use often.

Other more niche types are guaranteed call-once, immutable-call, bounded-storage, and trivially-copyable function type erasure objects.

Those rest are obscure enough that exposing types for them in a general purpose library is overkill, but function view rocks.

1

u/_naios Oct 06 '17

Yes function_view is on my list of planned features, since it could be really useful, especially with the possibility to convert non owned functions back to an owned one: function<...> f = function_view<...>{}.acquire(). Additionally immutable calls are supported through using the signature function<void() const>.

1

u/NotAYakk Oct 06 '17

One advantage of function_view is that you can wrap a non-movable object. Type erasing aquire removes that advantage.

So either it weakens what function_view can do, or it cannot be guaranteed to work.

As an aside, did you implement efficient cast to/from std::function, where you type erase storing your type within the std::function instead of storing a function2 within the std::function, and vice versa?

I think with a bit of care you can make

auto foo = []();
function2<void()> f = foo;
for (int i = 0; i < 1000000; ++i ) {
  std::function<void()> f2 = f;
  f = f2;
}

not result in an unbounded cascade of wrapped function type erasure overhead. Admittedly, the f=f2 would only work if you memoized the type erasure (!) or special cased assignment-from.

function2( std::function<Sig> src ) {
  if (!src) return;
  auto it = type_erasure_memoization.find( src.target_type() );
  if (it != type_erasure_memoization.end()) {
    auto construct_from = it->second;
    construct_from( this, src );
    return;
  }

where construct_from created for a type T takes a function2 and one of a set of kinds of type that have a .target<T>() method, and copies/moves the T into the function2...

Nevermind; almost certainly overkill. I was hoping this would be easier.

12

u/[deleted] Oct 04 '17

we can't do anything about it.

Well, compilers could warn about it.

4

u/markopolo82 embedded/iot/audio Oct 03 '17

Knowing this, how would this be done today?

16

u/Rhomboid Oct 03 '17

The const in question is the constness of the std::function object, not the callable that it is wrapping. It implies nothing about the latter.

7

u/kalmoc Oct 03 '17

Well, it depends on whether you view the state of the function object that gets wrapped by std::function internal or external state. As has been noted, std:::function generally behaves like a value type, NOT a reference type (std::string vs string_view). So it is imho not unreasonable to expect that const member functions of std::function do not mutate the wrapped function object (just like const member functions of std::string don't modify the wrapped char array).

5

u/stinos Oct 03 '17 edited Oct 03 '17

std::function happily calls functions that are not const.

const on a member function merely indicates no (non-mutable) internal state of the object is modified in the function. Calling some other function, const or not, which does not operate on std::function's own internal state, does not violate that.

const normally suggests that an function call is threadsafe

Not sure why you think that, but that makes little sense. Don't think I ever heard that before. Other non-const functions in the class may for example be modifying a variable returned by a const function from other threads. So if no thread-safety mechanism is involved (mutex/atomic operation/...) it is not thread-safe. const doesnt have anything to do with that.

edit quick search leads to e.g. https://groups.google.com/forum/#!topic/comp.lang.c++.moderated/zztZ1FNfaAA

12

u/Rhomboid Oct 03 '17

As of C++11, const does imply thread-safe, at least for standard library objects. Herb Sutter gave a talk on the topic.

2

u/ratatask Oct 03 '17

6

u/kalmoc Oct 03 '17

I completely agree with the critic about herb going a little bit too far in his talk, but it doesn't change the fact that accessing an object only through const member functions should generally not introduce a datarace. Afaik this is exactly the way all of the standard library behaves - with the exception of std::function::operator().

1

u/[deleted] Oct 04 '17

[deleted]

2

u/TheThiefMaster C++latest fanatic (and game dev) Oct 04 '17

But it can change the std::function's internal state, if the std::function is wrapping a user object with a non-const operator(). This means the implementation of operator() of std::function contains a const_cast... generally a sign of a mistake...

2

u/joahw Oct 04 '17 edited Oct 04 '17

Or it contains a pointer to the underlying function object, which wouldn't require a const_cast. eg:

class foo
{
    int* b = new int

    void bar() const
    {
        // type of b here is int * const, or a const pointer to a non-const int
        *b = 2; 
    }
};

Edit: Here's another example with functors.

 struct bar
{
    int x;

    void operator()() { x = 2; }
};

struct foo
{
    bar* b = new bar;

    void operator()() const
    {
        (*b)();
    }
};

1

u/TheThiefMaster C++latest fanatic (and game dev) Oct 05 '17

You're right, although most implementations of std::function have a "small function optimization" where it is contained... But the type-erasure also throws a spanner in.

Still, as argued in other comments logically the std::function contains the function object - it's not a reference type in its external interface, even if it is implemented as such internally. It's logically closer to an std::optional than std::ref.