4

Wt recommends to use forward declarations to avoid circular dependencies.

// Settings.h
#include <Wt/Dbo/Dbo.h>
#include <string>

class User; // Forward declaration of User Wt::Dbo object

class Settings 
{
public:
  Wt::Dbo::ptr<User> user;

  template<class Action>
  void persist(Action& a)
  {
    Wt::Dbo::belongsTo(a, user);
  }
};

 

// User.h
#include <Wt/Dbo/Dbo.h>
#include <string>

#include "Settings.h"

class User
{
public:
  Wt::Dbo::weak_ptr<Settings> settings;

  template<class Action>
  void persist(Action& a)
  {
    Wt::Dbo::hasOne(a, settings);
  }
};

However, when I use this Settings class in another cpp file, the program doesn't compile:

// test.cpp
#include "Settings.h"

error: C2079: 'dummy' uses undefined class 'User'

Possible solutions (which I do not like)

  1. A solution is to include in User.h in every cpp file that includes Settings.h, i.e.:

    // test.cpp
    #include "User.h"
    #include "Settings.h"
    

    I do not prefer this solution, because I have to remember to include User.h every time I include Settings.h.

  2. Another solution is to use the non-recommended DBO_EXTERN_TEMPLATES macro, i.e.

    // Settings.h
    ...
    class Settings
    {
    public:
       ....
    };
    
    DBO_EXTERN_TEMPLATES(Settings)
    

    I do not prefer this solution as this macro is not recommend, nor documented. DBO_EXTERN_TEMPLATES doesn't work with all compilers.

Question

a. What is the best/preferred methodology to overcome circular dependencies between Wt::Dbo objects avoiding the mentioned undefined class error?

b. Why does solution 1. works?

I created a new (general - not Wt::Dbo specific) question (with an MCVE), to clarify the specific situation: When are member functions of a templated class instantiated?

References

m7913d
  • 10,244
  • 7
  • 28
  • 56

2 Answers2

3

I'm not familiar with Wt::Dbo, but I don't believe the issue is specific with it. It is more a general C++ class design issue, which you need to work around/with; it is actually rather common in C++ projects.

For the "best/preferred method," that really is a matter of opinion. In your case, you can actually have both User.h and Settings.h include each other, if you still have the forward declarations.

For example, in Settings.h:

// include guard
class User;
#include "User.h"

class Settings { ... };

Then in User.h, you can do:

// include guard
class Settings;
#include "Settings.h"

class User { ... };

I know this seems odd, but it is a way to ensure you don't have to include both headers all the time. Alternatively, you just do that in one header, and ensure that is the one you always include.

In general, my preferred way is, in header files, only include what is absolute needed in the header, and forward declare the rest. In the source files, I then include the headers which are actually needed. The reason for this is because then, if I need to change one header file, I don't have to recompile all the source files which included that header; it improves the performance of the compilation process.

As for your question as to why solution 1 works, it's because of how you are including the files. In that specific example, you don't even need to include Settings.h in the source file, because User.h already does this. But let's look at how it looks once the pre-processor is done with it.

When you include User.h, it first includes Settings.h. An include basically copies the contents into the current file where the include occurred. So, effectively, your User.h would look something like this:

// User.h
#include <Wt/Dbo/Dbo.h> // contents from this would be here
#include <string> // contents from this would be here

// Settings.h
#include <Wt/Dbo/Dbo.h> // contents NOT included, due to previous include and include guards
#include <string> // same as above

class User; // Forward declaration of User Wt::Dbo object

class Settings 
{
public:
  Wt::Dbo::ptr<User> user;

  template<class Action>
  void persist(Action& a)
  {
    Wt::Dbo::belongsTo(a, user);
  }
};

class User
{
public:
  Wt::Dbo::weak_ptr<Settings> settings;

  template<class Action>
  void persist(Action& a)
  {
    Wt::Dbo::hasOne(a, settings);
  }
};

What you can see now, is that when the Settings class is being defined, the User is already forward declared and can be used by the Settings class. When User is then defined, it has the full definition of Settings to work with. In your test.cpp file now, both Settings and User are fully defined, and hence can be used.

I hope this helps :)

ChrisMM
  • 8,448
  • 13
  • 29
  • 48
  • I, too, only try to include necessary files in header files, but as every file that includes `Settings.h` has to include `User.h`, I don't think any recompilation is avoided by not including `User.h` in `Settings.h`, is it? – m7913d Dec 21 '19 at 08:54
  • I know the contents expands to what you have shown, but I'm wondering when the definition of the `User` class is used (instead of the forward declaration) as the `User` class is only used before its definition? Is the template `Wt::Dbo::ptr` instantiated at the end of the file? – m7913d Dec 21 '19 at 09:01
  • Is your solution an ill-formed program according to https://stackoverflow.com/a/59435158/7621674? – m7913d Dec 21 '19 at 10:05
  • The `User` class is not actually used before it's definition; it just looks like it is. `Wt::Dbo::ptr` is presumably a wrapper around a pointer, and pointers do not need the full definition of a class, until it is instantiated / used. Since you aren't actually instantiating a `User` object, or calling any of its functions in the `Settings` class, then it is perfect fine, and is still well formed. – ChrisMM Dec 21 '19 at 11:31
0

Based on the answer of ChrisMM, another solution is to forward declare your class at the top of its header file:

Settings.h:

// include guard
class Settings;
#include "User.h"

class Settings { ... };

Users.h:

// include guard
class User;
#include "Settings.h"

class User { ... };

The advantage of this approach is that you only have to forward declare the class in its own header file and are allowed to just include the file in any other (header) file that need it.

m7913d
  • 10,244
  • 7
  • 28
  • 56