C++ smart pointers and the Service Locator (anti-?)pattern
Note that the "old" version should store the logger in a std::unique_ptr in main, rather than explicitly calling new and delete.



The simplest possible thing would be something like this:

struct Services // previously `Engine`
{
    Logger logger;
};

int main()
{
    {
        Services services;
        services.logger.log("yep");
    }

    //services.logger.log("nope"); // obviously doesn't compile
}


To use the logger "somewhere else" we can pass it into an object constructor by reference, (or pass the entire Services object by reference):

struct Object
{
    explicit Object(Logger&amp; logger):
        logger(&amp;logger) { }

private:

    Logger* logger;
};


If necessary, the object can store the logger (or Services) by reference (which has the side-effect of preventing us defining assignment operators for Object as we can't re-assign a reference), or by pointer (which allows us to define assignment operators, but can also be reassigned to a nullptr (for better or worse)).

Either way, we still depend on the lifetime of the Logger being longer than the lifetime of the Object. With C++, this is usually enforced simply by the implicit order of destruction of stack objects (and any child objects they contain), e.g.:

{
    Services services;
    Object object(services.logger);

} // destruction in reverse order: first object, then services.




It might seem like we can use std::shared_ptr and std::weak_ptr to enforce the above constraint (longer service lifetime), but it adds overhead and complexity, and can lead to more serious issues. When we promote a weak_ptr to a shared_ptr, the new shared_ptr also "owns" the service. So the lifetime is no longer defined by Services, but by the service user as well.

If we enforce the Object lifetime constraint properly ourselves, we don't need this. If we don't enforce that constraint, then the exact moment of a service being destroyed becomes very difficult to determine, which leads to similar problems as with Singleton objects.

This article has some more arguments against using std::shared_ptr, and points out that what we actually need is some sort of checked pointer or borrowed reference for debugging. Unfortunately, this doesn't exist in the C++ standard library.



Note that if it's actually necessary to swap out, or destroy a service part-way through use, we can't store any pointer or reference to the service. It must be fetched from Services every time it's accessed. This is probably quite difficult to enforce though.



In short:


Avoid using this pattern if possible.
Pass dependencies by reference in constructors (or other functions).
Ensure that lifetimes are well-defined and easy to understand (i.e. avoid std::shared_ptr).
Ensure that the lifetime of the Engine / Services exceeds the lifetime of anything that uses them.
Don't move or copy the individual services, or the Services class.




And to answer your questions:


Yes.
Allowing direct access to the service member, returning a raw pointer from an accessor function, or returning a reference from an accessor function are all reasonable options. The first is simplest unless you have some other constraints (enforcing access through an interface, or enforcing const correctness).
I'd argue not (see above).