-2

I need have classes which implement similar things (different types of neural networks), but with somewhat different interfaces.

Doing research, I want to run the program with different neural network in the composing class. I want to be able to substitute and of existing types. It would be nice to handle this by run-time start up parameter, but compile-time placement would work as well.

I see at least 5 approaches with their drawbacks which stops me from using them:

  1. Implement base class with virtual functions and place pointer to base class into composing class. Main drawbacks: risks of object slicing, hell of cloning functions, assignment and comparison operators for the derived classes, dynamic_cast or similar dirty tricks to identify real object type for apply type-specific functions.
  2. Use template for neural network type. Drawback: this will require most of code to be moved to templates and, as the result I will have to put lots of implementation code into header files.
  3. Unify interfaces by adding placeholder functions. Drawback: interfaces will become dirty hell.
  4. Use C++ preprocessor with #ifdef blocks to separate two versions of code. Drawback: it is dirty copy/paste of blocks of code and with more than 2 classes comes to hell.
  5. Use adapter pattern which will provide unified interface and hide the details. Drawback: actually, it will be implemented with the pointer to implementation, so will suffer both from (1) and (2), but (2) will be only in adapter interface.

Are there better ways in the modern C++ to implement this? Something tells me that I can build some kind of solution around blocks constructed with std::function, but I am not sure yet.

Damir Tenishev
  • 1,275
  • 3
  • 14
  • 1
    For 2. you don't need to move any implementation to a header if you know that you only have a predetermined set of types. You can explicitly instantiate them in a single translation unit. Some of the problems with 1. should also not really be issues, e.g. if the classes are designed well, there shouldn't be a need to determine the real type in an implementation. (https://en.wikipedia.org/wiki/Liskov_substitution_principle) – user17732522 Apr 04 '23 at 20:14
  • _"risks of object slicing"_ isn't a real problem in any other sense than _"when creating a program, one may make mistakes"_. If you don't need runtime polymorphism, I'd go with templates though. – Ted Lyngmo Apr 04 '23 at 20:30
  • @user17732522, can you show what you mean for 2? I can't see how can I avoid putting templates into headers. On (1) the problem is that interfaces are differ, for example, layered NN have layers and some other types (like graphs without layers) don't. So, although they have a lot in common, tuning and other parts may differ; would it relate to tuning only, I would return a specific object from NN class for tuning interface, but there are many things which differ. – Damir Tenishev Apr 04 '23 at 20:31
  • Btw, why is putting the templates into headers an issue? If you don't like that, keep the implementation in the `.cpp` file but instantiate the types you need explicitly. I'd recommend not doing that though – Ted Lyngmo Apr 04 '23 at 20:32
  • @TedLyngmo, I see where you coming from, but I personally prefer safe programming, which here means that I don't want to give other programmers a higher chance to make silly mistakes. One thing is a mistake, another thing is bad architecture which requires a programmer to check code in many places to avoid a mistake. Templates would be good, but I accustomed to insulation (strong hiding of details), so including implementation to other files gives extra risks of unrevealed coupling, not speaking about compile time increase in large project. – Damir Tenishev Apr 04 '23 at 20:36
  • @TedLyngmo, can you please share a link with an example for you idea? I just missing how this should work in this case. I mean what exactly should be kept where. – Damir Tenishev Apr 04 '23 at 20:38
  • @DamirTenishev I'm pretty used to safe programming myself and it's never stopped me from using templates :-) For explicit instantiation, check [Class template#Explicit instantiation](https://en.cppreference.com/w/cpp/language/class_template#Explicit_instantiation). It means that you can keep your implementation details mostly hidden in your `.cpp` but you then need to explicitly instantiate every class and function with all the combinations of template parameters they are supposed to support. I've done that ... once. No fun maintenance. – Ted Lyngmo Apr 04 '23 at 20:42
  • @DamirTenishev For 2. see e.g. https://stackoverflow.com/questions/2351148/explicit-template-instantiation-when-is-it-used. For 1. if a function needs to access an interface specific to some derived type through a base reference, then the base should really offer a `virtual` member function providing this part of the interface. Otherwise you are not following the substitution principle. Or alternatively the function should be taking a reference to the derived type, in which case it is no problem to use the extended interface. Or maybe there should be intermediate bases in the hierarchy. – user17732522 Apr 04 '23 at 20:45
  • @TedLyngmo I guess the "safe programming" was in reference to object slicing. – user17732522 Apr 04 '23 at 20:47
  • "_extra risks of unrevealed coupling_": Not sure what you mean here. – user17732522 Apr 04 '23 at 20:48
  • @user17732522 I think the _"safe programming"_ was in reference to revealing implementation details when putting all the templates in header files, but I don't really see how that makes it any less safe. – Ted Lyngmo Apr 04 '23 at 20:49
  • With "extra risks of unrevealed coupling" I mean that if some template or class which is responsible for implementation details is put into header file, another programmer could start using it, not realizing that this is my implementation detail. Let's say he would like it as an utility or intermediate thing. Tomorrow when I refactor my code, this coupling will stop his code from compiling. So, he will be able to see and use my classes or templates, which I didn't intend to expose. – Damir Tenishev Apr 04 '23 at 21:56
  • @DamirTenishev approaches to the problem of exposing implementation details differ, but having Python background I dare say "so what" or rather "that's what code review is for". C++ gives plenty of ways to violate access restrictions anyway, but it doesn't mean everything should be hidden behind PIMPL. Unless agreed otherwise on a per-project basis of course. – alagner Apr 04 '23 at 22:33
  • @alagner, Python is a little bit different here. This is a high-level script language with the goal to implement fast small projects with very safe (and computationally expensive) tools build in to the language to support an idiom "code fast to reach very specific goal". C++ is mostly low-level language for large or even huge projects where such approach kicks back quite soon. In Python everything is open and visible since nobody plans to implement system-level code in it (games, etc.) and use shared pointers, virtual classes, etc. Anyway, consider this just as my research how to make better. – Damir Tenishev Apr 04 '23 at 22:54
  • @DamirTenishev my point is: there's always a tradeoff between assuming your coworkers follow conventions used in the project vs they try to hack through anything visible. Take C and Linux kernel as a flipside, everything is open and private is ruled by a naming and review. If you're pursuing very good access control (for whatever reason) then agreed, templates might be troublesome in that manner, but don't forget tricks like ptr-to-private-member in template instantiation which in the end might lock you in pimpl-hell anyway. – alagner Apr 04 '23 at 23:27
  • @alagner, you mix two things: intentional hacking and usage by mistake. Of course, any code could be broken intentionally; I don't fight this. My goal to avoid traps in code when people could use something not knowing that this is internal; could forget to implement virtual assignment or compare operator and end up with object slicing. I pay debt to good programmers and code reviews, but I still rely to compiler much more. If something could be avoided, it should. Again, let's consider the original goal "as is", not implications or project practices. – Damir Tenishev Apr 05 '23 at 02:01
  • Ok, no peroblem. Anyway, more on topić: have you considered considered type erasure-based polymorphism with value semantics or using variant? Both remove risk of object slicing and provide value semantics. On the other hand they deprive the programmer of some tools like languagae provided casts so they not always are a good fit. – alagner Apr 05 '23 at 04:30
  • @TedLyngmo, I've added my solution below. Could you please comment on it and let me know if you meant this one with template explicit instantiation usage? If not, could you please provide a piece of code to explain your approach? – Damir Tenishev Apr 07 '23 at 22:23

1 Answers1

0

Well, I found the solution, really don't know why I blundered yesterday, overcomplicating things.

The solution works for compile time. For run-time I would use option (1) from the original question.

The overall idea is to have overloaded functions with parameters of the specific classes which will allow compile-time polymorphism to choose the correct function which will take correct type to setup, tune, etc.

Solution

class NNType1 { … };
class NNType2 { … };

using NeuralNetwork = NNType1;
// using NeuralNetwork = NNType2;

class Creature {
    NeuralNetwork nn;
public:
  template <typename T> void setupNN(NNType1&);
  template <typename T> void tuneNN(NNType1&);
};

template<> Creature::setupNN(NNType1& nn) {
…
}

Actually instead of overloaded functions, the templates are used to simplify class declaration avoiding the list of overloaded functions for all types.

Now all the code which uses custom part of interfaces is put into these setupNN/tuneNN functions which are specific to concrete classes NNType1/NNType2/etc. For the generic code of Creature these functions hide implementation details.

Profits

  1. Easy to implement
  2. Now code is split into small functions without if/switch
  3. No RTTI information used, no dynamic_casts, etc.

Any comments, corrections, improvement suggestions?

Damir Tenishev
  • 1,275
  • 3
  • 14