0

Let's say I have a class Squad with a dynamic array of Units. I'm looking for a way to avoid #includeing "Unit.h" into "Squad.h" and users of the latter. The global idea is avoiding #includes leading to other ones - a pretty annoying thing.

I've found a simple solution like this:

// Squad.h
class Unit;
class Squad {
private:
  Unit*  units;
  size_t units_num;
public:
  void do_something_with_units(); // its implementation requires Unit to be a full type
};
// Squad.cpp
#include "Unit.h" // it's fine (and required) in Squad.cpp
void Squad::do_something_with_units() { // implements
}
// squad_user.h
// I want this whole file to be satisfied with only
// a forward-declaration of Unit (coming from Squad.h)
// provided it doesn't interact with Unit directly
void foo() {
  Squad squad;
  squad.do_something_with_units(); // works!
}

(Putting forward-declared Units in a std::vector inside Squad fails because it needs Squad's users to #include Unit themselves, otherwise the vector can't allocate.)

Question number one: is such practice acceptable? Of course, I'm not going to rewrite the guts of std::vector (and others) every time, but making a header with algorithms like template<typename T> T* reallocate(T*, size_t current, size_t needed) for later #includes in .cpp seems bearable.

Question number two: is there a better solution? I know about the pimpl idiom but dislike it for frequent small heap allocations.

By the way, what should I do with this when I need a more complicated data structure like std::unordered_map? Replacing an array isn't hard but there are "worse" cases like this one.

passing_through
  • 1,778
  • 12
  • 24
  • You will certainly get more bugs by using some homebrew data structures instead of `std::vector`. I do not think it is worth for avoiding some includes. What you can do is to template the `Squad` and using the template whenever you would use `Unit`, then you only need to know `Unit`s size when you instantiate this template. – n314159 Jan 30 '20 at 23:29
  • @n314159 thanks for an unusual idea but doesn't it need every `Squad` user to `#include` `Unit` anyway? I think it does, unfortunately. – passing_through Jan 30 '20 at 23:48
  • What version of C++ are you using? C++17 dropped the requirement that `Unit` be a complete type when instantiating `std::vector`, at least when using the standard allocator. – JaMiT Jan 31 '20 at 01:00
  • 1
    Related but tangential: if you aren't familiar with the [tag:pimpl-idiom], you might be interested in learning it. – JaMiT Jan 31 '20 at 01:02
  • @JaMiT `std::vector` users still need to `#include` `Unit` if it ever allocates (it does!) so this doesn't help in my case. – passing_through Jan 31 '20 at 01:11
  • @JaMiT thanks, I know about pimpl. I don't like the idea of allocating small memory chunks on the heap (which happens even in the default constructor!) so I'm looking for something else. – passing_through Jan 31 '20 at 01:13
  • 1
    @passing_through Whenever the `std::vector` allocates, your own version with `Unit*` should probably also allocate, for which it needs to know the size of `Unit` and hence needs to `#include` it. I just think what you want to do is not really feasible from a logical point of view. – n314159 Jan 31 '20 at 08:18
  • @passing_through You are describing a situation that does not make sense to me, perhaps because I am filling in blanks and your real problem lies in how *you* filled in those blanks. You might benefit from providing a full [mre] that demonstrates 1) that a forward declaration is insufficient when using a vector and 2) that a forward declaration is sufficient for a manually-allocated array. (Make sure you test-compile using C++17 conventions -- Stack Overflow assumes the current standard when not otherwise specified.) – JaMiT Jan 31 '20 at 16:09
  • I should probably mention that in this case a [mre] does not have to be a complete program. I am envisioning the example as two versions of the header file for `Squad` (already mostly done), plus a source file that uses `Squad` in a way that compiles when using the array version, yet fails to compile when using the vector version. (I do mean "compile" rather than "link" -- the implementation file for `Squad` is probably not needed for the example.) – JaMiT Jan 31 '20 at 16:27
  • @JaMiT OK, I rewrote the question. – passing_through Feb 01 '20 at 10:20

1 Answers1

1

At a guess, a key issue here is that vector is defined to be a useful class, handling several things (like copying) automatically. The cost of full automation is that the value type (Unit) needs to be a complete type when the containing class (Squad) is defined. To remove this cost, some of the automation must be given up. Some things are still automated, but now the programmer needs to be aware of the Rule of Three (or Five). (Full automation morphs this rule into the Rule of Zero, which is trivial to follow.)

The kicker is that any workaround would also require knowledge of the Rule of Three, so they do not end up being better than the vector approach. They might look better initially, but not after all the bugs are fixed. So, no, don't invent complex data structures; stick with vector.


is such practice acceptable?

In general, the practice is acceptable if done correctly. Typically, though, it's not warranted. Furthermore, your implementation is unacceptably incomplete.

  1. Your Squad class has a raw pointer that is not initialized, allowing it to pick up a garbage value. You need to initialize your data members (especially since they are private, hence only the class can initialize them).

  2. Your raw pointer is intended to own the memory to which it points, so your class needs to follow the Rule of Three (or Five). Your class needs to define (or delete) a copy constructor, a copy assignment operator, and a destructor. All of these definitions will (likely) require a full declaration of Unit so you want the definitions in your implementation file alongside do_something_with_units (i.e. not defined inline in the header file).

Is this being nitpicky? Not really. Addressing these omissions leads us to the realm of "not warranted". Let's look at your new class definition.

// Squad.h
class Unit;
class Squad {
private:
  Unit*  units;
  size_t units_num;
public:
  Squad();
  Squad(const Squad &);             // implementation requires Unit to be a full type
  Squad & operator=(const Squad &); // implementation requires Unit to be a full type
  ~Squad();                         // implementation requires Unit to be a full type

  void do_something_with_units();   // implementation requires Unit to be a full type
};

At this point, it would be possible to implement the default constructor in the header file (if it initializes units to nullptr instead of allocating memory), but let's entertain the possibility that it's no big deal to put it in the implementation file along with the other new functions. If you can accept that, then the following class definition works, with similar requirements for the implementation file.

// Squad.h
#include <vector>
class Unit;
class Squad {
private:
  std::vector<Unit> units;
public:
  Squad();                          // implementation requires Unit to be a full type
  Squad(const Squad &);             // implementation requires Unit to be a full type
  Squad & operator=(const Squad &); // implementation requires Unit to be a full type
  ~Squad();                         // implementation requires Unit to be a full type

  void do_something_with_units();   // implementation requires Unit to be a full type
};

I made two changes: the raw pointer and size were replaced by a vector, and the default constructor now requires Unit to be a full type for its implementation. The implementation of the default constructor might be as simple as Squad::Squad() {}, as long as the full definition of Unit is available at that point.

And that is probably what you were missing. The vector approach works if you provide explicit implementations for constructing, destructing, and copying, and if you put those implementations in your implementation file. The definitions might look trivial since the compiler does a lot of work behind the scenes, but those functions are what impose the requirement that Unit be a complete type. Take their definitions out of the header file, and the plan works.

(This is why the commentors were confused about why you thought a vector would not work. Outside the constructor, the times when vector needs Unit to be a complete type are exactly the times when your implementation would need Unit to be a complete type. Your proposed implementation is more work for no additional benefit.)

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • Wow, my problem with using `std::vector` actually came down to defining `Squad::Squad()` and "the rule of five" manually! While testing, I didn't think about them being generated automatically. Thanks a lot for opening my eyes! UPD: just want to add that I'm well-aware of member initialization and copy-move-destruct tricks with constructs like raw pointers. I omitted them in my example for reducing distraction from the main topic. – passing_through Feb 01 '20 at 17:17