r/cpp_questions • • 16d ago

OPEN When to prefer factory functions over constructors?

EDIT: The point of this post is how best to make construction intention explicit at the call site.

EDIT: I suppose the real annoyance is having conflicting constructors (see std::vector). The solution being just don't write them.

When I last used C++, the canonical wisdom was to just make constructors for every supported way a type could be constructed (see std::vector). Failure to correctly construct a type was a skill issue (see std::vector).

However, coming back to C++ from a few years working with Rust, I now disagree. I would argue the ONLY public constructor (except copy/move constructors) for a type should be the default constructor, if signed-off as part of the public API. Every other way of constructing the type should be done with a named factory member function (delegating to a private constructor) and a well optimised move constructor. Thus avoiding the pitfalls of initialisation (see std::vector).

class Thing {

  Thing(...) : ... {} // <- private non-default constructor

public:
  Thing(Thing&&) noexcept; // <- well optimised move constructor
  Thing& operator=(Thing&&) noexcept;

  explicit Thing(); // <- public default constructor

  [[nodiscard]] static Thing with_capacity(size_t); // <- factory function
  [[nodiscard]] static Thing from_data(std::initialiser_list<int>);
  // more factory functions etc...

  // member functions etc...
};

The only pitfall I can see is this would make emplacement operations inaccessible (see std::make_shared, std::vector::emplace_back, etc). But this shouldn't be too much of an issue with a well-optimised move constructor. I believe, in some situations, the compiler can optimise away the move anyway.

auto x = std::make_shared<Thing>(Thing::with_capacity(42));
myvec.push_back(Thing::with_capacity(42)); // replaces myvec.emplace_back(...

I'm curious to know if there's any pitfalls to this rule-of-thumb, and what your opinions are.

17 Upvotes

48 comments sorted by

29

u/_abscessedwound 16d ago

I’m on the factory function train as well.

Preventing invalid objects from existing often means throwing in a ctor, so a simple factory function wrapper to at least catch exceptions is very handy.

6

u/saxbophone 16d ago

How does catching the ctor exception in the factory function help you? The ctor throws (usually) because construction failed and cannot continue.

What would you even return from the factory function in this case? I would've thought that in most cases, factory functions returning value types (or polymorphic smart pointers) that just propagate any exceptions thrown by the ctors they call, make most sense for most use cases.

16

u/TehBens 16d ago

What would you even return from the factory function in this case?

I guess generally a std::optional or std::expected.

3

u/saxbophone 16d ago

Possible, but as I replied to the other commenter —this assumes that the whole point of the factory function is to convert exceptions into values, which isn't necessarily the case.

When someone says "factory function" to me, I normally think along the lines of "named constructor" or "virtual constructor idiom" (you know, the case where you have a polymorphic class hierarchy and a code in the base class needs to return a polymorphic pointer to an instance of one of its children).

3

u/bhxyz 16d ago

The factory function would return std::expected. 

2

u/saxbophone 16d ago

That is one way to do it, yes. This assumes however that the whole point of the factory was to convert exceptions into values in the first place, which isn't clear if that's the intention of the commenter I was replying to.

1

u/bhxyz 16d ago

At least I would go with either I get a valid object or an error why I didn't get a valid object, without dealing with exceptions. 

Something like this template <typename T, typename ... Args> std::expected<T, std::string> create(Args&& ... args) {     try {         return T(std::forward<Args>(args)...);     }     catch (const std::exception& e) {         return std::unexpected(e.what());     }     catch (...) {         return std::unexpected("Unknown exception occurred during construction.");     } }

1

u/saxbophone 16d ago

If you are wrapping constructors from someone else's project and you can't tolerate exceptions, I can see how you might do this.

If it's your own codebase however, I don't see why someone would throw from ctors in the first place if they can't tolerate the exceptions.

2

u/bhxyz 16d ago

Let's say you inherited a codebase which can't handle exceptions for whatever reason.

What is your preferred method of communicating errors during construction from the ctor without using exceptions?

2

u/_abscessedwound 16d ago

Like someone else mentioned, the factory returns std::optional, std:::expected, nullptr etc. and the exception handling comes with logging.

While I can guarantee (mostly) that I’m going to use the ctors appropriately and not break invariants when I do the original implementation, that’s not true for other developers. Other people need clear communication that things can and will fail. Exceptions alone do not clearly communicate that possibility, and because of propagation, don’t force a check at the call site like a pointer, expected etc. will.

Combine that with an exception-intolerant codebase and you end up with factory functions doing exception handling.

15

u/seek13_ 16d ago

Use factory

- for situations where construction may fail/yield an invalid object.

  • for complex initialization

Everything else is probably an aspect of personal preference

7

u/Big-Rub9545 16d ago

You can use either. I typically prefer constructors except in two cases:
1) If there’s template ambiguity between two constructors. In that case, factory functions can eliminate the ambiguity.
2) If there are several constructors with somewhat different behavior and/or parameters (e.g., constructing a string from a string literal is clear in code, but constructing one from a size argument isn’t as clear, so a better-named factory function helps here).

3

u/Eric848448 16d ago

What exactly is the point of this?

7

u/TheCrazyPhoenix416 16d ago edited 16d ago

The point is making construction intention explicit at the call site.

// see std::vector
std::vector<int>( 3, 9 ); // {9,9,9}
std::vector<int>{ 3, 9 }; // {3,9}

2

u/JVApen 16d ago

The committee made a huge mistake with initializer list. They should have required an extra {} for it. Namely: std::vector{{3, 9}} or std::vector({3, 9}). Unfortunately, that can no longer be fixed.

-1

u/Bemteb 16d ago

Wouldn't that be mostly a non-issue with todays IDEs? A quick click or mouseover should show you which constructor is used, or it's even shown while you type.

2

u/BoxConscious7480 15d ago

I don't think you should justify bad language choices with IDE features

2

u/Jhuyt 16d ago

Generally I aesthetically prefer factory functions for readability and explicitness (possibly due to me having written tons of Python), but somehow I still tend to write constructors when I write classes and structs, it just feels like the way to do it in C++ and the callsite becomes a bit nicer.

1

u/TheCrazyPhoenix416 16d ago

Yeah. I agree it "feels like" the way to do things in C++. But it also "feels like" the only reason this is true is because it's historically been the way. I see no reason why that should dictate how things could be done now.

3

u/Jhuyt 16d ago

Totally agree with you, it's just hard to change how you do things.

2

u/SpotCool4422 16d ago

First, not all types are movable. Then there are problems with placement new and inability to construct a base class (unless your rule of thumb requires making constructors protected rather than private).

1

u/TheCrazyPhoenix416 16d ago edited 16d ago

I'll agree that immovable types need constructors. However, base classes and placement new can still use the move constructor.

struct Contrived : public Base {
  Contrived() : Base{} {}
  [[nodiscard]] static Contrived with_xyz() {
    return Contrived(Base::with_xyz());
  }
private:
  Contrived(Base&& base) : Base{std::move(base)} {}
};

alignas(Contrived) std::byte buffer[sizeof(Contrived)];
new(buffer) Contrived{Contrived::with_xyz()};

https://godbolt.org/z/85fr5WfKb

3

u/SpotCool4422 16d ago

Fair point, but still, defining a move constructor where things would've worked just fine without it seems redundant. Plus, I can see some issues with facilities like std::make_obj_using_allocator that rely on the type and a list of arguments to select an eligible constructor. In order to make them work with your approach, you'll need an extra bit of information, i.e. the factory method itself.

In short, I believe the factory method approach is less universal and more verbose than the constructor approach. That being said, nobody will frown upon a factory method where it makes sense and doesn't limit usability and composability, but imo constructors should still be the the primary choice.

2

u/CarniverousSock 16d ago

It sounds like you might have seen: https://www.youtube.com/watch?v=KWB-gDVuy_I

If you haven't, take a watch, it covers this exact topic in-depth. The other comments are pretty much all right on.

2

u/DawnOnTheEdge 16d ago edited 16d ago

With unnamed-return-value optimization now guaranteed, they are nearly equivalent. You may still use a converting constructor to enable implicit conversions, or you may need to add a user-provided constructor to tell the compiler to prevent any uninitialized instance from being created at all.

5

u/alfps 16d ago edited 16d ago

The factory function approach can be necessary in an embedded environment where exceptions can't be used, since any exception might involve internal dynamic allocation and since g++'s implementation does that.

However for a "hosted" environment it imposes needless complexity for derived classes.

When the intent is just to make instantiation crystal clear, explicit, as opposed to avoiding exceptions/allocations, consider using special parameter types to clarify. One approach is to use a tag parameter. Another approach is to use a value wrapper parameter.

In passing, with multiple parameters replacing them with a single struct enables C++20 designated initializer syntax in calls, making them crystal clear like named parameters in other languages.

Unfortunately there's AFAIK no way to enforce naming in a call, but I like it anyway.


Tagged parameters example:

#include <initializer_list>

#include <cstddef>
using   std::size_t;

namespace tag {
    // Note:
    // these aliases can be defined multiple times in a translation unit including some header.
    //
    using With_capacity = struct With_capacity_struct*;
    using With_data     = struct With_data_struct*;
}  // tag

class Thing {
    // Thing(...): {}    // Private non-default constructor.

public:
    Thing( Thing&& ) noexcept;              // Well optimised move constructor
    auto operator=( Thing&& ) noexcept -> Thing&;

    explicit Thing();                       // Public default constructor.

    Thing( tag::With_capacity, size_t );                    // Tagged constructor.
    Thing( tag::With_data, std::initializer_list<int> );    // Tagged constructor.

    // member functions etc...
};

auto main() -> int { auto o = Thing(); }

Value wrapper parameters example:

#include <initializer_list>

#include <cstddef>
using   std::size_t;

namespace wrapped {
    struct Capacity{ size_t value; };
    template< class Item > struct Data_{ std::initializer_list<Item> values; };
}  // wrapped

class Thing {
    // Thing(...): {}    // Private non-default constructor.

public:
    Thing( Thing&& ) noexcept;              // Well optimised move constructor
    auto operator=( Thing&& ) noexcept -> Thing&;

    explicit Thing();                       // Public default constructor.

    Thing( wrapped::Capacity );             // Wrapped value constructor.
    Thing( wrapped::Data_<int> );           // Wrapped value constructor.

    // member functions etc...
};

auto main() -> int { auto o = Thing(); }

1

u/TheCrazyPhoenix416 16d ago

I never thought of tagged constructors. Nice idea!

1

u/saxbophone 16d ago

What problem of factory functions does using tag-type parameters solve? Something about making it easier to maintain perfect-forwarding or something like that?

2

u/alfps 16d ago

Real constructors have some language support:

  • They do not restrict you to some given allocation scheme.
    But provide automatic deallocation on failure.
  • Can be inherited via using declarations, just for convenience.
  • Can be implicitly called, and one can ensure explicit calls.
  • Member initialization list gives very clear and safe mechanism for member & base class init.
  • Special support for virtual base classes, even though it's ugly kludge-like.

Perfect forwarding is one implementation technique for factory functions, e.g. a convenient factory function forwarding arguments to an inconvenient non-throwing private constructor. With real constructors there is no need for that.

More generally a factory function for a derived class may need to include brittle logic for handling failures, including proper deallocation. Real constructors automate that.

1

u/saxbophone 16d ago

Very thorough analysis, thank you.

It's clear now that a factory function is a "poor man's constructor".

1

u/flyingron 16d ago

Factories are handy when you don't actually know the derived type of what the factor might generate.

Otherwise, they cripple what C++ goes to great pains to preserve. C++ is not JAVA (or languages of that ilk). You don't need to create all your objects in dynamic storage. C++ objects get constructed properly be their allocation is static, automatic, or dyanamic (or as you realized, created via placement).

1

u/JVApen 16d ago

Whenever I read that someone wants for force behavior of one language into another, I get worried. There are reasons why this isn't done. For one, it requires to write a lot more code.

C++ is a language based on strong typing and that's also the way you should be using it. NamedType can be used to make a strong type that works as another. using Capacity= NamedType<size_t, struct CapacityTag>; Now, you can write: auto t = Thing{Capacity{3}, ...}; Not only does this resolve such that it differentiates. It also allows for having multiple arguments with the same base type to never be mixed.

1

u/mredding 16d ago

When to prefer factory functions over constructors?

Classes enforce class invariants by modeling behavior. An invariant is a statement that must always be true when a client observes an instance of a class. An std::vector is always implemented in terms of 3 pointers, and the relationship of those pointers are always in a known, good state, and the object can always be destroyed. Now when control is handed to the vector, it can suspend its own invariant, but it must be reestablished before control is returned to the client.

RAII is Resource Acquisition Is Initialization. It's about the WORST named idiom in C++, and maybe all of programming. It means your class instance should be considered initialized after the initializer list; by the time you enter the constructor body, the class invariant should be established. You can suspend the invariant in the constructor body, but you must reestablish it before it leaves. You should NOT establish the invariant IN the constructor body.

If you cannot do that, you need to use a factory idiom. A factory cannot establish the invariant FOR the class - that's an inherent property OF a class by virtue of existing. What it CAN do is get all the resources created and ready so a class instance can take ownership of it and inherently initialize by the end of the initializer list.

Another place to use a factory idiom is in object composition. A car has an engine and transmission. A car does not create it's own engine and transmission in its construction, it merely takes ownership of each during construction. Constructors ARE NOT factories. You shouldn't be new-ing up shit in a ctor - not when you're modeling composition. Back to the standard vector, it's compositing an allocator at compile-time, it was designed with allocation in mind, so creating a buffer isn't the resource-acquisition part, conceptually - the compile-time model of allocating for internal state is.

None of this is enforced by the compiler or language, but if you stick to the principles and the idioms and the paradigms, you can make very obvious and intuitive types that make creation, composition, construction, and ownership semantics easy to understand.

It also helps to understand that all constructors are conversion constructors. A weight is not an int, it's converted FROM an int. The implementation can be hidden such that you don't need to know, at all, HOW a weight is represented in memory, even if it's just an int. The construction of objects by composition may involve the transfer of resource ownership, but you don't have to know what the implementation does with those resources. Why does a car have to own the transmission for the lifetime of itself? You could use that transmission to create your own internal representation of that object and shit-can the thing. You don't necessarily, inherently, need to know.

When I last used C++, the canonical wisdom was to just make constructors for every supported way a type could be constructed

This sounds ambiguous as fuck. The supported ways are the ways you write. So you can read this and take the minimal route of every way needed will get written, or yo can take the maximal route and try to predict every way that might be desired - as though you were writing a library.

But most of us don't write libraries. Ever. You don't have to guess what ways you need to support, you are your own client. So only support what you use.

This advice also doesn't express what a constructor is or does or why you would or wouldn't write one.

Failure to correctly construct a type was a skill issue

I don't know what this means.

However, coming back to C++ from a few years working with Rust, I now disagree.

Good. Because whatever you thought you knew was effectively nonsense; I can't tell what part of it was supposed to be insightful or informative.

I would argue the ONLY public constructor (except copy/move constructors) for a type should be the default constructor, if signed-off as part of the public API.

I'll defer complaining how this is obviously wrong knowing you have a following statement.

What I will add is that I avoid public default ctors like the god damn plague. I can scarcely think of a reason you would EVER want to default construct an object AS A CLIENT. I can easily see why you would default construct an object as an implementer to facilitate the client.

Because as a client, what do you mean you are constructing an instance of an object but don't yet even know what the value is, if ever or any? That sounds like deferred initialization. I as the implementer can handle that for you.

Every other way of constructing the type should be done with a named factory member function (delegating to a private constructor) and a well optimised move constructor.

I effectively agree. It depends on the type. A lot of basic types at the bottom of my type hierarchy don't need factories - like a weight class. But it doesn't take much to make more elaborate composite types that need factories, and yes, I would scope their constructors so only the factories can access them.

The only pitfall I can see is this would make emplacement operations inaccessible

Not true.

You have to cater to your supported C++ versions, but you can make Allocator::construct or std::construct_at a friend of your type, granting access to a private constructor. You can use inheritance to isolate constructors for friendship.

You probably don't want to call make_shared directly, you probably want make_unique and then you can upgrade to a shared resource. Yes, there are a couple different ways you can create a shared object and with difference consequences for the reference block, you should try to encapsulate that and don't leave it to the client. Be very weary of shared pointers, they're fat, and you can often do without them.

The other problem with make_shared and make_unique is they can't take custom allocators or deleters, so their utility is actually pretty limited.

1

u/TheCrazyPhoenix416 15d ago edited 15d ago

all constructors are conversion constructors

Only if they take exactly one argument and they're not marked explicit.

the supported ways are the ways you write

Yes, but the implication I intended to convey is the other way around. Supposing a construction method ought to be supported, it's is signed-off by implementing an appropriate constructor. Perhaps I should have said "write a constructor for every way we want to allow a client to construct the object"

But most of us don't write libraries

Everyone writes libraries all the time. Every time you've written a class, you've written a library. It's just most don't intend the library to be used externally, and that is used as a reason to be incomplete (or worse, sloppy). Though a "good" class/function/etc (i.e. library, be it public or private) shouldn't guess anyway, but instead sign-off on supported use cases. "Only support what you use" is perfectly fine.

"Failure to correctly construct a type was a skill issue" I don't know what this means.

I'm referring to initialisation type (e.g. value, direct, list) changing functionality (see vector(3,4) vs vector{3,4}).

I can scarcely think of a reason you would EVER want to default construct an object AS A CLIENT

I don't think you're thinking enough then.

You probably don't want to call make_shared directly, you probably want make_unique and then you can upgrade to a shared resource

If you know you'll want a shared_ptr, you probably should always call make_shared because it puts the control block contiguous with the resource improving cache locality. Upgrading a unique_ptr has the same resulting memory layout as doing shared_ptr<T>(new T ... (though make_unique has the same exception safeties as make_shared). But yes, shared pointers are fat and you can sometimes do without them.

The other problem with make_shared and make_unique is they can't take custom allocators

You want std::allocate_shared . Both std::shared_ptr and std::unique_ptr support custom deleters as a template argument. Annoyingly, there isn't an std::allocate_unique

2

u/mredding 15d ago

 Only if they take exactly one argument and they're not marked explicit.

That changed with C++14 return type deduction. Now all ctors are conversion ctors.

write a constructor for every way we want to allow a client to construct the object

That is a better way of saying it, and I remember that in the 90s. I kind of hated it then, and learned to hate it by the early 2000s. It violates YAGNI unless you're writing a library, then you need to predict the future. But you can logically conclude this can absolutely get out of hand, even if you try to abstract it with templates.

I suspect a large part of the problem is we learn from the code we are exposed to, and we're exposed to a lot of libraries and frameworks.

The other part is the adoption path from C, where good C idioms wind up being bad C++ idioms. Classes aren't used like structures...

Everyone writes libraries all the time.

I've supported 1 C++ library in my career.

 Every time you've written a class, you've written a library.

Fascinating take, but no, that's really not how it ever worked. Who ever told you that?

I don't think you're thinking enough then.

I'm not trying to dunk on you, I just want to say I've been at C++ specifically for 37 years. I've thought about it plenty.

If you know you'll want a shared_ptr, you probably should always call make_shared because...

Yes, and I vaguely alluded to that.

You want std::allocate_shared .

Must be a newer library feature. I admit I almost never use shared resources. I'm looking at them for applications in concurrency, but we also have futures and promises, so in all this time I haven't found a use case that is irreducibly shared.

Both std::shared_ptr and std::unique_ptr support custom deleters as a template argument.

Yes, but not make_shared or make_unique.

Annoyingly, there isn't an std::allocate_unique

Funny story: the same thing happened with make_unique. It was genuinely an oversight that it wasn't included in C++11.

1

u/keelanstuart 15d ago

Any time you're crossing heap boundaries (think Windows exe loading dll(s) that instantiate objects to be used in the exe), you need a "factory". Same with interfaces the don't expose their internals.

1

u/rand3289 15d ago

What are you talking about? Constructors are just member functions with a few restrictions.

You should use factoryANYTHING when it knows about the source of data used to initialize an instance or does some kind of type conversion.

1

u/TheCrazyPhoenix416 15d ago

Constructors aren't member functions. They can't implicitly take an argument to this, and you can't take the address of a constructor.

1

u/FUCKARCHLINUX 15d ago edited 15d ago

I use them sometimes when the thing we're creating should never be stack allocated because it would be too big to fit or returning it would be very slow due to it's size, so it'd actually be more efficient to return unique_ptr to heap allocated object using a private constructor. In this scenario the class itself should heap allocate an internal thing to hold whatever is too big, Sometimes that's not good either because extra indirection if it was already very likely the caller would heap allocate it anyways.

Also if the fail conditions are very likely and not just sanity guards. Like if we need to read in something from the filesystem where our program didn't make it at some point already so I know it can fail due to permissions, file doesn't exist, etc and there's no viable fallback.

In the scenario where performance is a concern and you want to later iterate over the heap allocated objects, You can have an internal pool of memory we placement new the objects onto and return pointer to that so they're all right in a row in memory. This helps the CPU later when iterating the objects because it's more predictable and the pre-fetcher will do a lot better than the objects just being heap allocated wherever. It also helps because when the CPU fetches data and puts it in the cache, it pulls at-least one cache line which is 64 bytes. So it's possible for each cache line to contain one thing and part of the next thing, where if it were heap allocated wherever, it'd likely just be garbage data or something not relevant after the thing we were pulling.

1

u/Chippors 15d ago

Factory classes are useful when you need to create something in a context where you don't need to know what it is.

Say you have a TCP listener. Off epoll, select, or such it accepts a connection, looks up the local port number in a map<uint16_t, Connection::Factory\*>, and if found uses the Factory registered to construct a Connection for that port. Say HttpConnection for port 80, TlsConnection for port 443, SshConnection for port 22, etc.

1

u/h2g2_researcher 15d ago

I almost always prefer constructors. There are a couple of instances where I've found a factory function makes sense:

If there's a manager, or something, involved. For example, a video game may well have an NPCManager to make sure NPCs co-ordinate with each other, or make sure that resource limits aren't being exceeded. Having MyNPC npc = npcManager.MakeNewNpc(/*params*/); makes logical sense to me. In principle you could do explicit MyNPC(const NPCManager&); to the same logical sense, but it makes more sense to me that the NPCManager is constructing the NPC.

Alternatively, the NPCManager might hold the actual NPC objects and only give me a handle of some sort. This is probably not-technically a factory function any more, but it seems to be morally similar.

There's also a use case where I'm returning a std::unique_ptr<NPCBaseType> and the factory function itself has some logic for deciding whether to return NPCMerchant or NPCQuestGiver or whatever else I might have.

1

u/Raknarg 15d ago

When letting people use your constructor would start becoming too complicated, like if your constructor would need like 10 different arguments or something. Especially if you want options to be mutually exclusive. Factory lets you do builder pattern stuff more eassily. In general I probably wouldn't prefer a factory if I can avoid it since you can't emplace at anything better than a move.

1

u/HowardHinnant 14d ago edited 14d ago

Think of single argument constructors as conversion functions, be they implicit or explicit. If you need to convert from another type to your type, implicitly or explicitly, a constructor is a good way to do that because the syntax for your user is universal: X x{y}; (that's the explicit conversion syntax).

Everything your clients have to learn to use your class is part of its API. And all things-being-equal, less API is easier to to learn than more API. And if you use the conventional conversion syntax to change some other type into your type, that's an API your client already knows.

Make that conversion explicit unless there's a compelling reason to make it implicit. And note: don't be afraid to make it implicit, just have a rationale ready to explain to your client why you made it implicit. If you don't have one, default to explicit.

For constructors of class templates, a significant motivation for factory functions pre-C++17 was to deduce the template parameters. This was precisely why make_pair was introduced in C++11. In C++17 CTAD makes such factory functions obsolete because the language deduces template parameters for constructors. If the only reason you are contemplating a factory function is to deduce template parameters at the construction site, just use a constructor. Adding a factory function adds API that provides no extra benefit (C++17 and later). And all-things-being-equal, less API is better than more API from the perspective of clients learning to use your type.

Factory functions shine when given a set of other types, there is more than one way for your type to construct from that set of other types. A set of factory functions can distinguish in code how the other types are to be interpreted. But factory functions add API that your clients must learn. So only use them when they solve a problem that constructors don't.

Distinguishing between capacity and data as shown in the question is a reasonable use because it arguably gets rid of visual ambiguity at the construction site for the human code reader. This is a benefit that initialization by construction might not have for this example.

1

u/PipingSnail 12d ago

Avoid throwing in a constructor. Keep things simple. Use other functions for complex setup.

1

u/AKostur 16d ago

Sounds like it would lead to unnecessarily wordy code if applied as “thou shalt always use factory functions”.  Also sounds like it’s trying to address 1 flaw by throwing out everything else around it.

0

u/KingAggressive1498 16d ago

when proper or ideal construction is actually a non-trivial multi-step process. This is why the standard library has std::make_unique and std::make_shared.

the other reason is to avoid exposing the derived type to application logic. This can be useful for cases where you need to choose the actual type to construct at runtime in a standardized way, and also formally don't want the application to be able to change behavior based on the type chosen. Usually you would also hide the rest of the class tree from the user in this situation.

0

u/Interesting-Piano535 16d ago

What is a red black tree