0

I'm coding with std::map in C++11.

I've read this link, which told me that the C++11 standard guarantees that const method access to containers is safe from different threads.

As my understanding, it means that std::map::size() is thread-safe, because this function is declared as size_type size() const noexcept;.

Now I want to call the function std::map::find thread-safely. For example, if (mp.find(xxx) != mp.end()) {}. However, there are two versions of find, one is const and the other is non-const:https://en.cppreference.com/w/cpp/container/map/find.

So how do I know which version of find is calling? How can I force the const-version find to be called in order to get a thread-safe code?

I knew there was a const version of std::map::cend(), will if (mp.find(xxx) != mp.cend()) {} work as expected?

Yves
  • 11,597
  • 17
  • 83
  • 180
  • "*`std::map::size()` is thread-safe, because this function is declared as `const`*" - no, none of `std::map` methods are thread-safe, an object can be used concurrently only when it's not also being updated at the same time. – rustyx Aug 10 '21 at 08:24
  • @rustyx So it is NOT thread-safe that one thread is modifying the `std::map` while the other thread is calling `std::map::size`? – Yves Aug 10 '21 at 08:27
  • 1
    Calling `size()` while another thread is e.g. adding to a map would be a data race. You need to use a mutex. – rustyx Aug 10 '21 at 08:28
  • @rustyx alright, I just misunderstood that link... – Yves Aug 10 '21 at 08:29

3 Answers3

5

You could have used std::as_const.

if(std::as_const(mp).find(xxx)==mp.end())
{
    //do your thing
}
Karen Baghdasaryan
  • 2,407
  • 6
  • 24
  • 1
    `end` is non-const. You may want to use `as_const(mp).end()` or `mp.cend()`. Also, this is a C++17 function. – xskxzr Aug 10 '21 at 08:19
2

If mp is const then find will be the const version. However calling the non-const version is no different in terms of thread safety than calling the const version, the difference comes if you actually use the returned iterator to make modifications to the held value (if you read the answer you linked carefully it contains this distinction).

Note that only calling multiple const methods is thread safe, calling a non-const method from another thread at the same time is not thread safe.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
2

The general rule is that const is multiple-reader safe, and other operations are not.

But a number of exceptions are carved out. Basically, the non-const operations returning iterators or references to existing objects (no object is created) are also considered "reader" operations, and are multiple-reader safe. Using the non-const_iterators they return to modify the underlying data often has issues, but in many containers such modifications only cause contention if done on the same element that another operation is accessing.

So

if (map.find(foo) == map.end())

is safe to use with other operations that only read from the map object.

There are still good reasons to call the const operations. In there is std::as_const, permitting

if (std::as_const(map).find(foo) == map.cend())

which only calls const methods on map. You can write your own as_const easily:

template<class T>
std::add_const_t<T>& as_const( T& t ) { return t; }

(in you'll need to expand add_const_t). The std version adds

template<class T>
void as_const( T const&& t ) = delete;

to block some relatively pathological cases.

...

When thinking about thread safety, realize that it is a relational property. Two operations are relatively thread safe.

In the case of std containers, you have to think about how the operation reads or writes the container itself, which elements it reads or writes. While the standard is more technical, that will give you an intuitive understanding of what is allowed.

Methods that only read the container and return iterators or references to elements are sometimes non-const, but they themselves only read the container.

Operations that mutate or read elements are often only exclusive with other operations that mutate that element. In a vector, you are free to v[0] = 77; while another thread reads v[7] with no synchronization (outside of vector<bool>). But the moment you .push_back with insufficient capacity, you need to be synchronized with every other read.

This can be counter intuitive. Be careful, and read the documentation, if you are doing synchonization free access to a container. The intuition is only a first step.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524