C++ smart pointers and the Service Locator (anti-?)pattern












2














So I'm working on a game framework and one of the things I need are access to stateful "services" that can be accessed from all over. I was initially going to use the Singleton pattern but decided to try and come up with alternatives to avoid the short comings of that pattern.



So here is what I've got so far.



Main.cpp



#include "Engine.h"
#include "Logger.h"

int main(int argc, char* argv) {
Engine engine;

// old
{
auto logger = new Logger();

engine.provide(logger);
engine.getLoggerOld().log("old TEST");

engine.provide(nullptr);
delete logger;
}
engine.getLoggerOld().log("old TEST");//wont print, just calls empty funcs
// old


// new
{
auto splogger = std::make_shared<Logger>();

engine.provide(splogger);
engine.getLoggerNew()->log("new TEST");
}
engine.getLoggerNew()->log("new TEST");//wont print, just calls empty funcs
// new

return 0;
}


Engine.h



#pragma once

#include <memory>

#include "ILogger.h"
#include "NullLogger.h"

class Engine {
public:
Engine() {
//old
serviceLogger = &nullLogger;

//new
spLoggerService = std::make_shared<NullLogger>();
}

///////// old
public:
void provide(ILogger* service) {
if (service) {
serviceLogger = service;
} else {
serviceLogger = &nullLogger;
}
}

ILogger& getLoggerOld() const {
return *serviceLogger;
}
private:
ILogger* serviceLogger;
NullLogger nullLogger;

///////// old

///////// new
public:
void provide(std::shared_ptr<ILogger> loggerService) {
wpLoggerService = loggerService;
}

std::shared_ptr<ILogger> getLoggerNew() {
if (wpLoggerService.expired()) {
wpLoggerService = spLoggerService;
}
return wpLoggerService.lock();
}
private:
std::shared_ptr<ILogger> spLoggerService;
std::weak_ptr<ILogger> wpLoggerService;

///////// new
};


I'm a bit unsure on how I should implement the services, in the above example(s) "Engine"(The Service Locator) essentially is designed to never really "own" the services, and instead it only holds a reference to the services, it's then the externally determined if/how/when the services become invalid.



In the example sections labeled "old", these are basically how I saw this pattern implemented elsewhere. I don't like that its left up to how its used to determine if the pointers will be valid or not.



In the example sections labeled "new" I tried to implement the more modern smart pointers, I like the automatic cleanup of the expired references, etc. But I'm still not sure if I like the way the references are handled.



So I guess my questions are:




  1. Does it make sense to give total ownership of the service to the Engine(locator) and allow its life cycle to manage cleanup?

  2. Should I stay with the raw pointer "old" way I found in online examples?

  3. Should I keep going with the "new" smart pointer way?(one thing I'm not a fan of here is needing the if (wpLoggerService.expired()) check as well as the .lock() when accessing a service.(maybe there is a better smart pointer way to avoide this?)


Also if it makes sense for the Engine(Locator) to manage the life-cycle of the services, I was thinking of maybe making the "provide" function similar to that of the std::vector's emplace_back where you would do something like: engine.provide<Logger>(/*ctor args*/) and the .provide<T>() function would instantiate the object internally to prevent any ownership/access to it externally. (This just seems like a more reasonable way to let the Engine control its life-cycle, or is this dumb?)



Disclaimer: New to C++, first time using smart pointers, please be gentle :3










share|improve this question
























  • More context would be useful. Is there a fixed, known number of services? Do they each have a “do nothing” version? Do you really need a new object, or just runtime reconfigurations? Are there multiple engines? Can callers simply call via the engine rather than getting their own version of the logger?
    – Edward
    Dec 31 '18 at 11:07










  • @Edward In my use case, I can only imagine needing one instance of Engine. As for the Services, they will be added to overtime. Currently I have a "do nothing" version of each service. As far as how I'd see classes access the engine instance, I was thinking of providing it as a constructor argument to the base class of everything that requires engine access. So like entities.emplace_back<Entity>(engine) etc. That way I can avoid the "evil" global scope/singletons and everything stays relatively separate.
    – Hex Crown
    Dec 31 '18 at 12:07










  • It would be good if you could add to your question, explaining what you're trying to do. The current main doesn't really convey it. For instance, if the application is multi-threaded, that has a big impact on the design.
    – Edward
    Dec 31 '18 at 12:51
















2














So I'm working on a game framework and one of the things I need are access to stateful "services" that can be accessed from all over. I was initially going to use the Singleton pattern but decided to try and come up with alternatives to avoid the short comings of that pattern.



So here is what I've got so far.



Main.cpp



#include "Engine.h"
#include "Logger.h"

int main(int argc, char* argv) {
Engine engine;

// old
{
auto logger = new Logger();

engine.provide(logger);
engine.getLoggerOld().log("old TEST");

engine.provide(nullptr);
delete logger;
}
engine.getLoggerOld().log("old TEST");//wont print, just calls empty funcs
// old


// new
{
auto splogger = std::make_shared<Logger>();

engine.provide(splogger);
engine.getLoggerNew()->log("new TEST");
}
engine.getLoggerNew()->log("new TEST");//wont print, just calls empty funcs
// new

return 0;
}


Engine.h



#pragma once

#include <memory>

#include "ILogger.h"
#include "NullLogger.h"

class Engine {
public:
Engine() {
//old
serviceLogger = &nullLogger;

//new
spLoggerService = std::make_shared<NullLogger>();
}

///////// old
public:
void provide(ILogger* service) {
if (service) {
serviceLogger = service;
} else {
serviceLogger = &nullLogger;
}
}

ILogger& getLoggerOld() const {
return *serviceLogger;
}
private:
ILogger* serviceLogger;
NullLogger nullLogger;

///////// old

///////// new
public:
void provide(std::shared_ptr<ILogger> loggerService) {
wpLoggerService = loggerService;
}

std::shared_ptr<ILogger> getLoggerNew() {
if (wpLoggerService.expired()) {
wpLoggerService = spLoggerService;
}
return wpLoggerService.lock();
}
private:
std::shared_ptr<ILogger> spLoggerService;
std::weak_ptr<ILogger> wpLoggerService;

///////// new
};


I'm a bit unsure on how I should implement the services, in the above example(s) "Engine"(The Service Locator) essentially is designed to never really "own" the services, and instead it only holds a reference to the services, it's then the externally determined if/how/when the services become invalid.



In the example sections labeled "old", these are basically how I saw this pattern implemented elsewhere. I don't like that its left up to how its used to determine if the pointers will be valid or not.



In the example sections labeled "new" I tried to implement the more modern smart pointers, I like the automatic cleanup of the expired references, etc. But I'm still not sure if I like the way the references are handled.



So I guess my questions are:




  1. Does it make sense to give total ownership of the service to the Engine(locator) and allow its life cycle to manage cleanup?

  2. Should I stay with the raw pointer "old" way I found in online examples?

  3. Should I keep going with the "new" smart pointer way?(one thing I'm not a fan of here is needing the if (wpLoggerService.expired()) check as well as the .lock() when accessing a service.(maybe there is a better smart pointer way to avoide this?)


Also if it makes sense for the Engine(Locator) to manage the life-cycle of the services, I was thinking of maybe making the "provide" function similar to that of the std::vector's emplace_back where you would do something like: engine.provide<Logger>(/*ctor args*/) and the .provide<T>() function would instantiate the object internally to prevent any ownership/access to it externally. (This just seems like a more reasonable way to let the Engine control its life-cycle, or is this dumb?)



Disclaimer: New to C++, first time using smart pointers, please be gentle :3










share|improve this question
























  • More context would be useful. Is there a fixed, known number of services? Do they each have a “do nothing” version? Do you really need a new object, or just runtime reconfigurations? Are there multiple engines? Can callers simply call via the engine rather than getting their own version of the logger?
    – Edward
    Dec 31 '18 at 11:07










  • @Edward In my use case, I can only imagine needing one instance of Engine. As for the Services, they will be added to overtime. Currently I have a "do nothing" version of each service. As far as how I'd see classes access the engine instance, I was thinking of providing it as a constructor argument to the base class of everything that requires engine access. So like entities.emplace_back<Entity>(engine) etc. That way I can avoid the "evil" global scope/singletons and everything stays relatively separate.
    – Hex Crown
    Dec 31 '18 at 12:07










  • It would be good if you could add to your question, explaining what you're trying to do. The current main doesn't really convey it. For instance, if the application is multi-threaded, that has a big impact on the design.
    – Edward
    Dec 31 '18 at 12:51














2












2








2







So I'm working on a game framework and one of the things I need are access to stateful "services" that can be accessed from all over. I was initially going to use the Singleton pattern but decided to try and come up with alternatives to avoid the short comings of that pattern.



So here is what I've got so far.



Main.cpp



#include "Engine.h"
#include "Logger.h"

int main(int argc, char* argv) {
Engine engine;

// old
{
auto logger = new Logger();

engine.provide(logger);
engine.getLoggerOld().log("old TEST");

engine.provide(nullptr);
delete logger;
}
engine.getLoggerOld().log("old TEST");//wont print, just calls empty funcs
// old


// new
{
auto splogger = std::make_shared<Logger>();

engine.provide(splogger);
engine.getLoggerNew()->log("new TEST");
}
engine.getLoggerNew()->log("new TEST");//wont print, just calls empty funcs
// new

return 0;
}


Engine.h



#pragma once

#include <memory>

#include "ILogger.h"
#include "NullLogger.h"

class Engine {
public:
Engine() {
//old
serviceLogger = &nullLogger;

//new
spLoggerService = std::make_shared<NullLogger>();
}

///////// old
public:
void provide(ILogger* service) {
if (service) {
serviceLogger = service;
} else {
serviceLogger = &nullLogger;
}
}

ILogger& getLoggerOld() const {
return *serviceLogger;
}
private:
ILogger* serviceLogger;
NullLogger nullLogger;

///////// old

///////// new
public:
void provide(std::shared_ptr<ILogger> loggerService) {
wpLoggerService = loggerService;
}

std::shared_ptr<ILogger> getLoggerNew() {
if (wpLoggerService.expired()) {
wpLoggerService = spLoggerService;
}
return wpLoggerService.lock();
}
private:
std::shared_ptr<ILogger> spLoggerService;
std::weak_ptr<ILogger> wpLoggerService;

///////// new
};


I'm a bit unsure on how I should implement the services, in the above example(s) "Engine"(The Service Locator) essentially is designed to never really "own" the services, and instead it only holds a reference to the services, it's then the externally determined if/how/when the services become invalid.



In the example sections labeled "old", these are basically how I saw this pattern implemented elsewhere. I don't like that its left up to how its used to determine if the pointers will be valid or not.



In the example sections labeled "new" I tried to implement the more modern smart pointers, I like the automatic cleanup of the expired references, etc. But I'm still not sure if I like the way the references are handled.



So I guess my questions are:




  1. Does it make sense to give total ownership of the service to the Engine(locator) and allow its life cycle to manage cleanup?

  2. Should I stay with the raw pointer "old" way I found in online examples?

  3. Should I keep going with the "new" smart pointer way?(one thing I'm not a fan of here is needing the if (wpLoggerService.expired()) check as well as the .lock() when accessing a service.(maybe there is a better smart pointer way to avoide this?)


Also if it makes sense for the Engine(Locator) to manage the life-cycle of the services, I was thinking of maybe making the "provide" function similar to that of the std::vector's emplace_back where you would do something like: engine.provide<Logger>(/*ctor args*/) and the .provide<T>() function would instantiate the object internally to prevent any ownership/access to it externally. (This just seems like a more reasonable way to let the Engine control its life-cycle, or is this dumb?)



Disclaimer: New to C++, first time using smart pointers, please be gentle :3










share|improve this question















So I'm working on a game framework and one of the things I need are access to stateful "services" that can be accessed from all over. I was initially going to use the Singleton pattern but decided to try and come up with alternatives to avoid the short comings of that pattern.



So here is what I've got so far.



Main.cpp



#include "Engine.h"
#include "Logger.h"

int main(int argc, char* argv) {
Engine engine;

// old
{
auto logger = new Logger();

engine.provide(logger);
engine.getLoggerOld().log("old TEST");

engine.provide(nullptr);
delete logger;
}
engine.getLoggerOld().log("old TEST");//wont print, just calls empty funcs
// old


// new
{
auto splogger = std::make_shared<Logger>();

engine.provide(splogger);
engine.getLoggerNew()->log("new TEST");
}
engine.getLoggerNew()->log("new TEST");//wont print, just calls empty funcs
// new

return 0;
}


Engine.h



#pragma once

#include <memory>

#include "ILogger.h"
#include "NullLogger.h"

class Engine {
public:
Engine() {
//old
serviceLogger = &nullLogger;

//new
spLoggerService = std::make_shared<NullLogger>();
}

///////// old
public:
void provide(ILogger* service) {
if (service) {
serviceLogger = service;
} else {
serviceLogger = &nullLogger;
}
}

ILogger& getLoggerOld() const {
return *serviceLogger;
}
private:
ILogger* serviceLogger;
NullLogger nullLogger;

///////// old

///////// new
public:
void provide(std::shared_ptr<ILogger> loggerService) {
wpLoggerService = loggerService;
}

std::shared_ptr<ILogger> getLoggerNew() {
if (wpLoggerService.expired()) {
wpLoggerService = spLoggerService;
}
return wpLoggerService.lock();
}
private:
std::shared_ptr<ILogger> spLoggerService;
std::weak_ptr<ILogger> wpLoggerService;

///////// new
};


I'm a bit unsure on how I should implement the services, in the above example(s) "Engine"(The Service Locator) essentially is designed to never really "own" the services, and instead it only holds a reference to the services, it's then the externally determined if/how/when the services become invalid.



In the example sections labeled "old", these are basically how I saw this pattern implemented elsewhere. I don't like that its left up to how its used to determine if the pointers will be valid or not.



In the example sections labeled "new" I tried to implement the more modern smart pointers, I like the automatic cleanup of the expired references, etc. But I'm still not sure if I like the way the references are handled.



So I guess my questions are:




  1. Does it make sense to give total ownership of the service to the Engine(locator) and allow its life cycle to manage cleanup?

  2. Should I stay with the raw pointer "old" way I found in online examples?

  3. Should I keep going with the "new" smart pointer way?(one thing I'm not a fan of here is needing the if (wpLoggerService.expired()) check as well as the .lock() when accessing a service.(maybe there is a better smart pointer way to avoide this?)


Also if it makes sense for the Engine(Locator) to manage the life-cycle of the services, I was thinking of maybe making the "provide" function similar to that of the std::vector's emplace_back where you would do something like: engine.provide<Logger>(/*ctor args*/) and the .provide<T>() function would instantiate the object internally to prevent any ownership/access to it externally. (This just seems like a more reasonable way to let the Engine control its life-cycle, or is this dumb?)



Disclaimer: New to C++, first time using smart pointers, please be gentle :3







c++ design-patterns






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Dec 31 '18 at 9:03

























asked Dec 31 '18 at 8:38









Hex Crown

695




695












  • More context would be useful. Is there a fixed, known number of services? Do they each have a “do nothing” version? Do you really need a new object, or just runtime reconfigurations? Are there multiple engines? Can callers simply call via the engine rather than getting their own version of the logger?
    – Edward
    Dec 31 '18 at 11:07










  • @Edward In my use case, I can only imagine needing one instance of Engine. As for the Services, they will be added to overtime. Currently I have a "do nothing" version of each service. As far as how I'd see classes access the engine instance, I was thinking of providing it as a constructor argument to the base class of everything that requires engine access. So like entities.emplace_back<Entity>(engine) etc. That way I can avoid the "evil" global scope/singletons and everything stays relatively separate.
    – Hex Crown
    Dec 31 '18 at 12:07










  • It would be good if you could add to your question, explaining what you're trying to do. The current main doesn't really convey it. For instance, if the application is multi-threaded, that has a big impact on the design.
    – Edward
    Dec 31 '18 at 12:51


















  • More context would be useful. Is there a fixed, known number of services? Do they each have a “do nothing” version? Do you really need a new object, or just runtime reconfigurations? Are there multiple engines? Can callers simply call via the engine rather than getting their own version of the logger?
    – Edward
    Dec 31 '18 at 11:07










  • @Edward In my use case, I can only imagine needing one instance of Engine. As for the Services, they will be added to overtime. Currently I have a "do nothing" version of each service. As far as how I'd see classes access the engine instance, I was thinking of providing it as a constructor argument to the base class of everything that requires engine access. So like entities.emplace_back<Entity>(engine) etc. That way I can avoid the "evil" global scope/singletons and everything stays relatively separate.
    – Hex Crown
    Dec 31 '18 at 12:07










  • It would be good if you could add to your question, explaining what you're trying to do. The current main doesn't really convey it. For instance, if the application is multi-threaded, that has a big impact on the design.
    – Edward
    Dec 31 '18 at 12:51
















More context would be useful. Is there a fixed, known number of services? Do they each have a “do nothing” version? Do you really need a new object, or just runtime reconfigurations? Are there multiple engines? Can callers simply call via the engine rather than getting their own version of the logger?
– Edward
Dec 31 '18 at 11:07




More context would be useful. Is there a fixed, known number of services? Do they each have a “do nothing” version? Do you really need a new object, or just runtime reconfigurations? Are there multiple engines? Can callers simply call via the engine rather than getting their own version of the logger?
– Edward
Dec 31 '18 at 11:07












@Edward In my use case, I can only imagine needing one instance of Engine. As for the Services, they will be added to overtime. Currently I have a "do nothing" version of each service. As far as how I'd see classes access the engine instance, I was thinking of providing it as a constructor argument to the base class of everything that requires engine access. So like entities.emplace_back<Entity>(engine) etc. That way I can avoid the "evil" global scope/singletons and everything stays relatively separate.
– Hex Crown
Dec 31 '18 at 12:07




@Edward In my use case, I can only imagine needing one instance of Engine. As for the Services, they will be added to overtime. Currently I have a "do nothing" version of each service. As far as how I'd see classes access the engine instance, I was thinking of providing it as a constructor argument to the base class of everything that requires engine access. So like entities.emplace_back<Entity>(engine) etc. That way I can avoid the "evil" global scope/singletons and everything stays relatively separate.
– Hex Crown
Dec 31 '18 at 12:07












It would be good if you could add to your question, explaining what you're trying to do. The current main doesn't really convey it. For instance, if the application is multi-threaded, that has a big impact on the design.
– Edward
Dec 31 '18 at 12:51




It would be good if you could add to your question, explaining what you're trying to do. The current main doesn't really convey it. For instance, if the application is multi-threaded, that has a big impact on the design.
– Edward
Dec 31 '18 at 12:51










1 Answer
1






active

oldest

votes


















1














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& logger):
logger(&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:




  1. Yes.

  2. 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).

  3. I'd argue not (see above).






share|improve this answer





















  • Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
    – Hex Crown
    2 days ago











Your Answer





StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");

StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210630%2fc-smart-pointers-and-the-service-locator-anti-pattern%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes









1














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& logger):
logger(&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:




  1. Yes.

  2. 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).

  3. I'd argue not (see above).






share|improve this answer





















  • Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
    – Hex Crown
    2 days ago
















1














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& logger):
logger(&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:




  1. Yes.

  2. 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).

  3. I'd argue not (see above).






share|improve this answer





















  • Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
    – Hex Crown
    2 days ago














1












1








1






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& logger):
logger(&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:




  1. Yes.

  2. 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).

  3. I'd argue not (see above).






share|improve this answer












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& logger):
logger(&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:




  1. Yes.

  2. 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).

  3. I'd argue not (see above).







share|improve this answer












share|improve this answer



share|improve this answer










answered Dec 31 '18 at 19:03









user673679

2,3781925




2,3781925












  • Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
    – Hex Crown
    2 days ago


















  • Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
    – Hex Crown
    2 days ago
















Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
– Hex Crown
2 days ago




Back to the drawing board I go. Your post makes me thing I may have over complicated everything a bit to much, I'll see what I can do about making it simpler/following your suggestions. Thanks.
– Hex Crown
2 days ago


















draft saved

draft discarded




















































Thanks for contributing an answer to Code Review Stack Exchange!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


Use MathJax to format equations. MathJax reference.


To learn more, see our tips on writing great answers.





Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


Please pay close attention to the following guidance:


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210630%2fc-smart-pointers-and-the-service-locator-anti-pattern%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Terni

A new problem with tex4ht and tikz

Sun Ra