Preventing users from creating unnamed instances of a class [duplicate]
Asked Answered
D

4

22

For many RAII "guard" classes, being instantiated as anonymous variables does not make sense at all:

{
    std::lock_guard<std::mutex>{some_mutex};
    // Does not protect the scope!
    // The unnamed instance is immediately destroyed.
}

{
    scope_guard{[]{ cleanup(); }};
    // `cleanup()` is executed immediately!
    // The unnamed instance is immediately destroyed.
}

From this article:

Anonymous variables in C++ have “expression scope”, meaning they are destroyed at the end of the expression in which they are created.


Is there any way to prevent the user from instantiating them without a name? ("Prevent" may be too strong - "making it very difficult" is also acceptable).

I can think of two possible workarounds, but they introduce syntactical overhead in the use of the class:

  1. Hide the class in a detail namespace and provide a macro.

    namespace detail
    {
        class my_guard { /* ... */ };
    };
    
    #define SOME_LIB_MY_GUARD(...) \
        detail::my_guard MY_GUARD_UNIQUE_NAME(__LINE__) {__VA_ARGS__}
    

    This works, but is hackish.

  2. Only allow the user to use the guard through an higher-order function.

    template <typename TArgTuple, typename TF>
    decltype(auto) with_guard(TArgTuple&& guardCtorArgs, TF&& f)
    {
        make_from_tuple<detail::my_guard>(std::forward<TArgTuple>(guardCtorArgs));
        f();
    }
    

    Usage:

    with_guard(std::forward_as_tuple(some_mutex), [&]
    {
        // ...
    });
    

    This workaround does not work when the initialization of the guard class has "fluent" syntax:

    {
        auto _ = guard_creator()
                     .some_setting(1)
                     .some_setting(2)
                     .create();
    }
    

Is there any better alternative? I have access to C++17 features.

Dumah answered 30/11, 2016 at 9:15 Comment(7)
I think you're asking in the wrong direction. C++ developers should understand why lock_guard exists and why should it stay alive as long as the scope it protects stands. trying to enforce this via API or internal implementation is just redundant..Sightless
@Danh: the other question's answer is my "macro" workaround. Would you consider reopening this question if I make it more obvious that I'm looking for a non-macro-based solution? @David: for the lock_guard example, I agree. I was working on some non-allocation async chain generation with "fluent" syntax though, where it's very natural to have anonymous chains. But you still need a name, otherwise the storage for the chain dies too quickly. It's not as obvious as the lock_guard, and I'd like to prevent this mistake.Dumah
@VittorioRomeo There're other answers in that question. And, IMHO, if we can centralize all discussion about this in one place, it's better. Anyway, this answer looks very interestingPyridine
@VittorioRomeo so basically, you work on a stack-allocated version of std::async + future::then?Sightless
This one also related to this question: #915361Pyridine
"being instantiated as anonymous variables does not make sense at all" except when the result is bound to a reference and lifetime-extended. That's about the only way you can write a make_lock_guard before C++17. One possibility is to force the use of a make_ function and then mark it [[nodiscard]].Timmy
Does not answer this question, but clang-tidy has a check that looks for this pattern which already helps.Adaptive
S
6

The only sensible way I think about is to make the user pass the result of guard_creator::create to some guard_activator which takes a lvalue-reference as a parameter.

this way, the user of the class has no way but either create the object with a name (the sane option that most developers will do), or new it then dereference (insane options)

for example, you said in the comments you work on a non allocating asynchronous chain creator. I can think on an API which looks like this:

auto token = monad_creator().then([]{...}).then([]{...}).then([]{...}).create();
launch_async_monad(token); //gets token as Token&, the user has no way BUT create this object with a name 
Sightless answered 30/11, 2016 at 9:56 Comment(8)
MSVC++ sends her regardsPyridine
@Pyridine they fixed that glitch a long time agoSightless
rextester.com/DALS87730 is compiled successful with and without /Za. 1900 indicates this is VS2015Pyridine
@Pyridine You can compile with /we4239 to treat that extension as errorCreature
This looks very promising for my particular situation. I'll try it out ASAP - thanksDumah
If you change it to an rvalue-reference wouldn't it also work? But allow you to call a function where the guard-object is active during the function call. That contradicts the requirement my in the question (it is an unnamed object) - but the guard is still active when needed.Rorke
@HansOlsson rvalue reference can catch temporeries.Sightless
Yes, they can temporarily catch temporaries. But does that cause a specific problem? It lives during the time the function is called. However, I understand if you want to call two functions and want the lock to active for both it will not work - and for that reason lvalue-reference could be better. A user can make the same error by having two separate scopes with guard and function call; but that seems odd.Rorke
I
3

If have access to the full potential of C++17, you can expand the idea of using a static factory function into something usefull: guarantied copy elision makes the static factory function possible even for non-movable classes, and the [[nodiscard]] attributes prompts the compiler to issue a warning if the return value is ignored.

class [[nodiscard]] Guard {
  public:
    Guard(Guard& other) = delete;
    ~Guard() { /* do sth. with _ptr */ }
    static Guard create(void* ptr) { return Guard(ptr); }
  private:
    Guard(void* ptr) : _ptr(ptr) {}
    void* _ptr;
};

int main(int, char**) {
  Guard::create(nullptr);
  //auto g = Guard::create(nullptr);
}

Compile in Compiler Explorer

Induline answered 30/11, 2016 at 15:15 Comment(0)
L
0

You could use an extensible lint tool such as Vera++ https://bitbucket.org/verateam/vera/wiki/Home it lets you lint your code, you can create new rules using Python or tcl (I prefer Python)

A possible flow would be - after each commit, your CI system (e.g Jenkins) will run a job that executes Vera++ and validate such oversights, upon a failure a mail would be issued to the committer.

Lenardlenci answered 30/11, 2016 at 9:52 Comment(0)
E
0

The canonical way to prevent a class from being instantiated is by making its constructor private. To actually get one of the desired instances, you call a static method, which returns a reference to a constructed object.

class Me {
public:
    static Me &MakeMe() { return Me(); }
private:
    Me();
}; // Me

This doesn't help of course - but it'd probably make the programmer pause!

int main() {
    Me(); // Invalid
    Me m; // Invalid
    Me::MakeMe(); // Valid - but who'd write that?
    Me m = Me::MakeMe();
} // main()

I know this isn't a direct analog to the Guard instances that you describe - but maybe you could adapt the concept?

Emlyn answered 30/11, 2016 at 10:10 Comment(1)
I don't think this should even compile; you're binding an rvalue to an lvalue reference.Kaliope

© 2022 - 2024 — McMap. All rights reserved.