2

Is it correct that

if (std::lock_guard lk(m_state.mtx); true) {
  m_state.aborting = true;
}

and

{
  std::lock_guard lk(m_state.mtx)
  m_state.aborting = true;
}

are 100% identical?


Here is the background on my question: In Mastering the C++17 STL, the author gives a code example for a thread pool. It contains the following block (page 208):

~ThreadPool() {
  if (std::lock_guard lk(m_state.mtx); true) {  // <<--- Line of interest
    m_state.aborting = true;
  }

  m_cv.notify_all();
  for (std::thread& t : m_workers) { 
    t.join();
  }
}

The marked line uses the initializer part of an if-statement to create the lock_guard. This has been used in other questions in order to limit the scope of the lock_guard to the if-statement while also protecting the evaluation of the if-statement itself.

What confuses me in this example is that the if statement has no proper condition. According to the cppreference page on if,

If init-statement is used, the if statement is equivalent to

{
  init_statement
  if ( condition )
    statement-true
}

In this case, this would mean that if (std::lock_guard lk(m_state.mtx); true) {...} is equivalent to

{
  std::lock_guard lk(m_state.mtx);
  if (true) {
    m_state.aborting = true;
  }
}

Am I missing either a case where this is not 100% identical or a good reason why it has been written with an if-statement?

The same pattern is used in a different example on the next page, so I think that it was done on purpose and not a leftover from when there was an actual condition.

mrks
  • 8,033
  • 1
  • 33
  • 62
  • 2
    I believe the two fragments have identical behavior; the difference is purely stylistic. The `if` statement shaves a line of code, and perhaps makes the scoping more visible (at the expense of throwing off a reader that encounters this idiom for the first time). – Igor Tandetnik Jan 09 '21 at 14:59
  • I hope the author realizes that if he supplies an else-block ,he'll still have the lock. (probably not the intention) – engf-010 Jan 09 '21 at 15:14
  • 2
    IMHO this shouldn’t be used that way. Using an `if` without a real condition does not make sense, and will raise unnecessary questions in a code review, if the always `true` condition is an mistake or not. – t.niese Jan 09 '21 at 15:46
  • @engf-010 Seeing as the condition is `true`, the `else` block will never run even if present. – Igor Tandetnik Jan 09 '21 at 17:17
  • @Igor : Duh. Realy ? I though that was rather obvious. It's about (ab)usage of the if-initializer part ! – engf-010 Jan 09 '21 at 17:41
  • @engf-010 You seem to have suggested that adding an `else` block to this construct would in some way change its semantics; while I contend that it would not. Perhaps I misunderstood the thrust of your comment. What do you mean by "still have the lock"? Could you show an example that you believe would not work as intended? – Igor Tandetnik Jan 09 '21 at 17:45
  • @Igor : if something else than true was used for the condition a inexperienced/naieve/careless reader might think that the else block was 'unlocked'. – engf-010 Jan 09 '21 at 17:51

2 Answers2

3

They are semantically identical. Since the book seems to be all about the new C++ 17 features it makes sense the author uses it. But be careful: every new featured will be abused untill the dust settles and the common wisdom reaches a common consensus about good and less good ways of using the feature.

In my humble opinion this way just adds confusion (why is there an if construct if the block is executed unconditionally?) and I prefer the simple block. Time will tell if the author's way will become an acceptable or frowned upon construct. Or if nobody will care or if it will forever remain controversial.

bolov
  • 72,283
  • 15
  • 145
  • 224
1

I'm the author of Mastering the C++17 STL.

I'd already forgotten that construct, so there can't have been any important reason I used it. I believe Igor is correct: the choice would have been between

~ThreadPool() {
  if (std::lock_guard lk(m_state.mtx); true) {
    m_state.aborting = true;
  }
  m_cv.notify_all();
  for (std::thread& t : m_workers) {
    t.join();
  }
}

and something such as

~ThreadPool() {
  if (true) {
    std::lock_guard lk(m_state.mtx);
    m_state.aborting = true;
  }
  m_cv.notify_all();
  for (std::thread& t : m_workers) {
    t.join();
  }
}

which does the same thing (arguably clearer) but takes one extra line of code in the physical book.

Personally I don't like to write just { to open a block; I would always rather write if (true) {. This helps the reader understand that the if wasn't accidentally omitted (or merge-conflicted away); the programmer really intends the block to run always. If you cuddle all your braces all the time, as I (often) do, then it also maintains the invariant that you never have a { or } sitting alone on a line — for whatever that might be worth.

The other thing going on there is that it's specifically in the context of notifying on a condition variable. I definitely used to write

std::lock_guard lk(m_state.mtx);
m_state.aborting = true;
m_cv.notify_all();

with the notify happening inside the scope of the lock; over a period of years, I heard "you should release the lock before notifying" often enough that eventually I also started saying it (even though I still have no good intuition for why that should be the case). This was actually the subject of my fourth-ever SO question, five years before I wrote the book: C++11 std::condition_variable: can we pass our lock directly to the notified thread?

So I may have been distracted (underthinking) and/or self-conscious (overthinking) while writing this particular snippet, which might have led me to try out this particular weird construct.

The example on the next page is actually the condition-variable thing exactly:

void enqueue_task(UniqueFunction task) {
  if (std::lock_guard lk(m_state.mtx); true) {
    m_state.work_queue.push(std::move(task));
  }
  m_cv.notify_one();
}

Thread-safety-wise, this is exactly equivalent to

void enqueue_task(UniqueFunction task) {
  std::lock_guard lk(m_state.mtx);
  m_state.work_queue.push(std::move(task));
  m_cv.notify_one();
}

except that people kept telling me not to put the notify inside the lock. So the if (...; true) version grudgingly satisfies that popular wisdom while spending only one line of code instead of two.

It's even possible that I had originally notified under the lock, and then the book's one technical reviewer said "don't notify under the lock," and then I made this late-breaking change specifically to deal with that in the minimal number of lines. I don't remember whether that was the actual story, but it's quite plausible to me.

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159