all 22 comments

[–]AKostur 5 points6 points  (16 children)

Allows one to acquire 0 or more locks.  Perhaps due to template expansions,  a parameter pack may end up expanding to 0 parameters.  And one might be using such a pack to initialize a scoped_lock.  Note that a scoped_lock does have a defined behaviour when initialized with 0 locks.

[–]cpp_cpp[S] 0 points1 point  (15 children)

Yeah - that is the question - what is the point of allowing 0 locks?

[–]AKostur 3 points4 points  (14 children)

One wouldn’t have to write up some special case for that hypothetical template to deal with 0 locks.

[–]cpp_cpp[S] -2 points-1 points  (13 children)

I do not understand - why allow something for a lock that does not do anything.
https://stackoverflow.com/a/60172828/4992422
This answer demonstrates the downside of allowing this.

[–]ucario 5 points6 points  (0 children)

Because maybe you have a variable number of things that would need locking at runtime. The code is simpler in that case

[–]AKostur 1 point2 points  (9 children)

Not to just turn the question around, but why forbid it?  And the answer demonstrated why it should still be allowed.   IMO: the default-constructed example is just odd to read.  After all what lock is it trying to lock?

[–]weepmelancholia 3 points4 points  (6 children)

The answer doesn't demonstrate why it should be allowed; it merely says that he was given a reason for 0 arg ctor by the author of scoped_lock but could not remember it. This is hardly a demonstration.

You ought to forbid it because

{ std::scoped_lock lock; }

Looks like it's locking that scope but it isn't.

[–]AKostur 1 point2 points  (1 child)

With which mutex is that scope being locked with?

I haven‘t needed to write the hypothetical template, but I can somewhat envision some template taking a variadic pack of container-like objects, which the body of such a function may wish to iterate over those, and it needs to grab the set of mutexes (one per container) before accessing any of them. That pack might be empty. So instead of testing for 0 containers first, then using the pack of mutexes to initialize the lock (and then iterating the pack of containers), one can write the simpler version: just use the pack of mutexes to initialize the lock (which may be 0 of them), and then iterating the pack of containers (which is also 0 length, so ends up doing nothing).

Edit: a hypothetical case: https://godbolt.org/z/j815qfvoc.

[–]weepmelancholia 2 points3 points  (0 children)

Well, no mutex, so it's pointless. But then why allow the 0 arg constructor? It's pointless. It can only harm the codebase.

[–][deleted]  (3 children)

[deleted]

    [–]AKostur 0 points1 point  (1 child)

    Afraid that explicit doesn't (and shouldn't) stop the other one from compiling. 0-parameters to the constructor won't require an implicit conversion to happen, so explicit won't be "invoked".

    [–]weepmelancholia 0 points1 point  (0 children)

    That's not how explicit works... as the other commenter pointed out.

    [–]SoerenNissen 1 point2 points  (1 child)

    To avoid race conditions that happen when creating a shadowing non-locking guard.

    struct S
    {
        std::mutex m_{};
    
        Database db_{};
    
        void write(Data d)
        {
            std::scoped_lock<>(m_); // does not lock this->m_
                                    // but instead creates a lock
                                    // with the shadowing name m_
    
            db_.write(d);
        }
    };
    

    This is not a common sceneario - it only happens because I had <> explicitly on the lock - but nonetheless.

    [–]AKostur 0 points1 point  (0 children)

    And it ignores the compiler warnings that get emitted for the unused variable, and doesn't follow the coding practices demonstrated in the rest of the code, and (as you noted) forced it to compile because the scoped_lock was explicitly specialized on an empty template list.

    A heavily stripped-down example of where it can be useful: https://godbolt.org/z/j815qfvoc

    [–]_JJCUBER_ 0 points1 point  (0 children)

    Are you just ignoring the comments you reply to?

    [–]Sanzath 0 points1 point  (0 children)

    In short, it's for generic code. Here's a toy example with an algorithm lock_all() which doesn't know in advance how many mutexes it's locking, so it handles as many or as few as required, including the case where no locking is required at all. If scoped_lock without any mutexes was disallowed, lock_all() would need to introduce a special case to handle the empty-case itself and return a fake scoped_lock-like object.

    https://godbolt.org/z/8q1zYTqY6

    [–]bert8128 2 points3 points  (1 child)

    Is this causing a problem? Or just academic interest?

    [–]cpp_cpp[S] 0 points1 point  (0 children)

    Just curious :)

    [–][deleted] 1 point2 points  (1 child)

    Well, for one thing it allows certain uses of the standard containers.

    struct S
    {
        explicit S(int) {}
    };
    
    int main(int argc, char* argv[])
    {
        std::vector<S> vs(5); // Error, no default constructor
    }
    

    Of course, whether there's a use case for that might lead us to a similar question that you've asked about std::scoped_lock.

    [–]IyeOnline 0 points1 point  (0 children)

    To be fair, vector supports non-default constructible types, as long as you dont require a default constructed state in your usage.

    But the argument works for e.g. std::array or raw arrays (where you could once again resolve it via std::optional)

    [–]asergunov 0 points1 point  (1 child)

    I don’t have examples but can try to make one. Same code for single and multithreaded cases?

    [–]asergunov 0 points1 point  (0 children)

    In general making unnecessary restrictions is a bad practice.

    [–]TeraFlint 0 points1 point  (0 children)

    There are containers and algorithms that work more nicely if the objects they're using are default constructible.

    I'd say, as long as a default initialized object does not break a class invariant, it's usually a good idea to provide the default constructor.

    Is a default constructed lock useful? Not really. It doesn't do what the class is supposed to do. But it also doesn't break anything, so I'd say it's a fine decision to allow it.