No, it is never bad form to break out of any loop early unless you are aware of an invariant that will always break early. I have seen loops process the first element only:
for (auto item : itemCollection) {
// do the 95% stuff
break;
}
This code should never be used, unfortunately I have seen it in production code several times in the past. Of course this prompted a code review.
Anyway, this does not appear to be your case. I would still reconsider the design of your algorithm though. Why break out of the loop just because one element somewhere in the container fails a check? Why not skip that element? Maybe looking at the big picture will reveal a better way of doing whatever you are trying to do. It is difficult for me to give more guidance without more of the code and the overall design.
I think it is a poor strategy to make Derived_1::Impl
derive from Base::Impl
.
The main purpose of using the Pimpl idiom is to hide the implementation details of a class. By letting Derived_1::Impl
derive from Base::Impl
, you've defeated that purpose. Now, not only does the implementation of Base
depend on Base::Impl
, the implementation of Derived_1
also depends on Base::Impl
.
Is there a better solution?
That depends on what trade-offs are acceptable to you.
Solution 1
Make Impl
classes totally independent. This will imply that there will be two pointers to Impl
classes -- one in Base
and another one in Derived_N
.
class Base {
protected:
Base() : pImpl{new Impl()} {}
private:
// It's own Impl class and pointer.
class Impl { };
std::shared_ptr<Impl> pImpl;
};
class Derived_1 final : public Base {
public:
Derived_1() : Base(), pImpl{new Impl()} {}
void func_1() const;
private:
// It's own Impl class and pointer.
class Impl { };
std::shared_ptr<Impl> pImpl;
};
Solution 2
Expose the classes only as handles. Don't expose the class definitions and implementations at all.
Public header file:
struct Handle {unsigned long id;};
struct Derived1_tag {};
struct Derived2_tag {};
Handle constructObject(Derived1_tag tag);
Handle constructObject(Derived2_tag tag);
void deleteObject(Handle h);
void fun(Handle h, Derived1_tag tag);
void bar(Handle h, Derived2_tag tag);
Here's quick implementation
#include <map>
class Base
{
public:
virtual ~Base() {}
};
class Derived1 : public Base
{
};
class Derived2 : public Base
{
};
namespace Base_Impl
{
struct CompareHandle
{
bool operator()(Handle h1, Handle h2) const
{
return (h1.id < h2.id);
}
};
using ObjectMap = std::map<Handle, Base*, CompareHandle>;
ObjectMap& getObjectMap()
{
static ObjectMap theMap;
return theMap;
}
unsigned long getNextID()
{
static unsigned id = 0;
return ++id;
}
Handle getHandle(Base* obj)
{
auto id = getNextID();
Handle h{id};
getObjectMap()[h] = obj;
return h;
}
Base* getObject(Handle h)
{
return getObjectMap()[h];
}
template <typename Der>
Der* getObject(Handle h)
{
return dynamic_cast<Der*>(getObject(h));
}
};
using namespace Base_Impl;
Handle constructObject(Derived1_tag tag)
{
// Construct an object of type Derived1
Derived1* obj = new Derived1;
// Get a handle to the object and return it.
return getHandle(obj);
}
Handle constructObject(Derived2_tag tag)
{
// Construct an object of type Derived2
Derived2* obj = new Derived2;
// Get a handle to the object and return it.
return getHandle(obj);
}
void deleteObject(Handle h)
{
// Get a pointer to Base given the Handle.
//
Base* obj = getObject(h);
// Remove it from the map.
// Delete the object.
if ( obj != nullptr )
{
getObjectMap().erase(h);
delete obj;
}
}
void fun(Handle h, Derived1_tag tag)
{
// Get a pointer to Derived1 given the Handle.
Derived1* obj = getObject<Derived1>(h);
if ( obj == nullptr )
{
// Problem.
// Decide how to deal with it.
return;
}
// Use obj
}
void bar(Handle h, Derived2_tag tag)
{
Derived2* obj = getObject<Derived2>(h);
if ( obj == nullptr )
{
// Problem.
// Decide how to deal with it.
return;
}
// Use obj
}
Pros and Cons
With the first approach, you can construct Derived
classes in the stack. With the second approach, that is not an option.
With the first approach, you incur the cost of two dynamic allocations and deallocations for constructing and destructing a Derived
in the stack. If you construct and destruct a Derived
object from the heap you, incur the cost of one more allocation and deallocation. With the second approach, you only incur the cost of one dynamic allocation and one deallocation for every object.
With the first approach, you get have the ability to use virtual
member function is Base
. With the second approach, that is not an option.
My suggestion
I would go with the first solution so I can use the class hierarchy and virtual
member functions in Base
even though it is a little bit more expensive.
Best Answer
I think the fundamental problem is a combination of language features (or lack thereof) of C++. Both the library code and the client code are reasonable (as evidenced by the fact that the problem is far from obvious). If the lifetime of the temporary
B
was suitable extended (to the end of the loop) there would be no problem.Making temporaries life just long enough, and no longer, is extremely hard. Not even a rather ad-hoc "all temporaries involved in the creation of the range for a range-based for live until the end of the loop" would be without side effects. Consider the case of
B::a()
returning a range that's independent of theB
object by value. Then the temporaryB
can be discarded immediately. Even if one could precisely identify the cases where a lifetime extension is necessary, as these cases are not obvious to programmers, the effect (destructors called much later) would be surprising and perhaps an equally subtle source of bugs.It would be more desirable to just detect and forbid such nonsense, forcing the programmer to explicitly elevate
bar()
to a local variable. This is not possible in C++11, and probably never will be possible because it requires annotations. Rust does this, where the signature of.a()
would be:Here
'x
is a lifetime variable or region, which is a symbolic name for the period of time a resource is available. Frankly, lifetimes are hard to explain -- or we haven't yet figured out the best explanation -- so I will restrict myself to the minimum necessary for this example and refer the inclined reader to the official documentation.The borrow checker would notice that the result of
bar().a()
needs to live as long as the loop runs. Phrased as a constraint on the lifetime'x
, we write:'loop <= 'x
. It would also notice that the receiver of the method call,bar()
, is a temporary. The two pointers are associated with the same lifetime, hence'x <= 'temp
is another constraint.These two constraints are contradictory! We need
'loop <= 'x <= 'temp
but'temp <= 'loop
, which captures the problem pretty precisely. Because of the conflicting requirements, the buggy code is rejected. Note that this is a compile-time check and Rust code usually results to the same machine code as equivalent C++ code, so you need not pay a run-time cost for it.Nevertheless this is a big feature to add to a language, and only works if all code uses it. the design of APIs is also affected (some designs that would be too dangerous in C++ become practical, others can't be made to play nice with lifetimes). Alas, that means it's not practical to add to C++ (or any language really) retroactively. In summary, the fault is on the inertia successful languages have and the fact that Bjarne in 1983 didn't have the crystal ball and foresight to incorporate the lessons of the last 30 years of research and C++ experience ;-)
Of course, that's not at all helpful in avoiding the problem in the future (unless you switch to Rust and never use C++ again). One could avoid longer expressions with multiple chained method calls (which is pretty limiting, and doesn't even remotely fix all lifetime troubles). Or one could try adopting a more disciplined ownership policy without compiler assistance: Document clearly that
bar
returns by value and that the result ofB::a()
must not outlive theB
on whicha()
is invoked. When changing a function to return by value instead of a longer-lived reference, be conscious that this is a change of contract. Still error prone, but may speed up the process of identifying the cause when it does happen.