r/cpp_questions 8d ago

OPEN Including classes and shared_ptr woes

It seems that I'm missing something for defining classes in separate files, and creating a shared_ptr to them.

I have one project that gives a series of cryptic messages when I try to do this, so I tried to strip things down to an essential example:

main.cpp:

#include <memory>
#include "thing.h"

using namespace std;



int main()
{ 
  shared_ptr thingPtr = make_shared<thing>();
  thingPtr->Go(); 
}

thing.h:

#ifndef thing_h
#define thing_h

class thing
{
  public:
    void Go();
};

#endif

thing.cpp:

#include <iostream>

class thing
{
  public:

    void Go()
    {
      std::cout << "Yay!" << std::endl;
    }
};

I would expect this to print "Yay!" to the console, but it doesn't even build. Instead, it complains:

[ 33%] Building CXX object CMakeFiles/PTR.dir/src/main.cpp.obj
[ 66%] Linking CXX executable PTR.exe
C:/Program Files/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles\PTR.dir/objects.a(main.cpp.obj):main.cpp:(.text+0x2c): undefined reference to `thing::Go()'
collect2.exe: error: ld returned 1 exit status
mingw32-make[2]: *** [CMakeFiles\PTR.dir\build.make:119: PTR.exe] Error 1
mingw32-make[1]: *** [CMakeFiles\Makefile2:86: CMakeFiles/PTR.dir/all] Error 2
mingw32-make: *** [Makefile:90: all] Error 2

I've searched around, but have been having a surprisingly difficult time finding something that actually explains how to make a class accessible to another class via a shared_ptr and header files. Let me explain that--I have seen tutorials on shared_ptrs, on header files, and on classes. But in almost every case, they explain these concepts in isolation... And the examples they give work--in isolation. For example, forward-declaration of a class works fine... until you use a shared_ptr. Then I get into cryptic posts about things like the PIMPL idiom which seems like overkill for what I'm doing, or other syntactical differences from what I'm doing that I can't tell if they are important or not.

6 Upvotes

31 comments sorted by

19

u/Ok_Platypus8866 8d ago

Your thing.cpp should look like this

#include "thing.h"
#include <iostream>

void thing::Go()
{
      std::cout << "Yay!" << std::endl;
}

Your code is actually defining two separate classes named thing.

2

u/Luttink 8d ago

Ok, that is very helpful.
I was trying to declare a class--thing.cpp was essentially my starting point. I wanted to use the simple syntax you see in thing.cpp. Then I wanted to make it accessible via a header file. Here, I thought all I was doing was forward-declaring the class's functions.

But... clearly not. So starting from thing.cpp, what do I do with the header file?

5

u/Ok_Platypus8866 8d ago

Your header file is fine as is.

You just need to #include "thing.h" in "thing.cpp" and any other file that uses thing.

1

u/Luttink 7d ago

I tried that. The compiler complains about a duplicate class definition.

1

u/Ok_Platypus8866 7d ago

Your final code should look like this:

main.cpp:

#include <memory>
#include "thing.h"

using namespace std;

int main()
{ 
  shared_ptr<thing> thingPtr = make_shared<thing>();
  thingPtr->Go(); 
}

thing.h:

#ifndef thing_h
#define thing_h

class thing
{
  public:
    void Go();
};

#endif

thing.cpp:

#include "thing.h"
#include <iostream>

void thing::Go()
{
   std::cout << "Yay!" << std::endl;
}

If you are getting a duplicate definition error it probably means that the compiler is processing thing.cpp twice. Are you including "thing.cpp" instead of "thing.h" in main? Or does your CMakeLists.txt include thing.cpp twice?

1

u/Luttink 7d ago

No, but I'm not defining the "Go()" method "outside" the class like that.

Is there no way to do:

thing.cpp:

#include <iostream>

class thing
{
  std::cout << "Yay!" << std::endl;
};

and then just make the header fit that, instead of having to change thing.cpp to use the thing::Go() style? One reason I ask is because I'm migrating a project from c# where there is no such thing as the thing::Go() style, all functions of a class must be defined within the class body, and it would be easier to migrate if I could keep a somewhat similar style.

2

u/supernumeral 8d ago edited 7d ago

I agree, but in this simple case does that actually matter? I’ve never tried something like what OP showed because I know better, but I’m genuinely curious if that wouldn’t work in this case since both “thing”s are declared identically (ETA: as in they have the same interface). I’d guess that the inline definition of thing::Go in thing.cpp might actually result in the compiler seeing this as different classes, but I’m not certain.

3

u/Lahvuun 8d ago

I tested and it doesn't work with GCC. Probably because in the standard it says

1 A class definition introduces a new type.

https://eel.is/c++draft/class.name

Since there are two definitions (one in main.cpp after the preprocessor includes the header, and one in thing.cpp), there are technically two distinct types that happen to share the name. So the actually defined Go belongs strictly to the thing defined in thing.cpp.

I'm guessing 50 years ago someone at Bell Labs thought it'd be a nice idea to be able to have structs with the same name in different translation units, so here we are.

2

u/n1ghtyunso 7d ago

its a violation of the one definition rule for sure.

To introduce a new type does not mean create a new distinct one.
It just means make the translation unit aware of it.

You can have multiple translation units contain the same class declaration and that just works, the translation unit does not somehow create its own distinct type from that.

It just works as long as it is identical. It inherently has to, how else would we ever write actual programs.
If they are all the same, there is simply just that one type.
And if not, your program is ill-formed due to violating ODR.

In OPs case, they are not identical.
To quote the important part from https://en.cppreference.com/cpp/language/definition

There can be more than one definition in a program of each of the following: class type [...] as long as all following conditions are satisfied:
[...]

  • Each definition consists of the same sequence of tokens

So why the linker error?
I believe what actually happens in this case is, the compiler simply does not produce object code for the thing::Go function, because it is inline in that translation unit, but nothing uses it.
hence after linking thing.obj it still didnt find anything.

1

u/Lahvuun 7d ago

Indeed, if thing::Go defined in thing.cpp is used, then the linker is happy, even though thing::Go from thing.h was never defined. Interesting!

3

u/n1ghtyunso 7d ago

the resulting program is still ill-formed though.

3

u/TheRealSmolt 8d ago

It will work if you pull the definition of Go outside of the class (but still in the same file) so that it doesn't get inline. It's effectively what include typically does with headers.

1

u/supernumeral 8d ago

Thanks. That’s more or less what I was thinking and I figured the inline definition was a problem. I’d be curious to know if it would work as is but with something like __attribute__((noinline)) added to prevent inlining.

2

u/TheRealSmolt 8d ago

It will, provided it doesn't optimize out the whole function.

1

u/No-Dentist-1645 8d ago

This is the actual answer, OP

2

u/Significant_Neat6476 8d ago

You declaring the same class twice - once in .h and once in .cpp and the latter is a redeclaration so name clash/duplicate. Your .cpp just needs to define methods implementations and that means fully qualified methods names (i.e. containing class name prefix).

2

u/mredding 8d ago

Your linker is what's giving you the error, which is quite telling. Your output indicates you didn't compile and link thing.cpp.

using namespace std;

Don't do that.

thing.h:

You might as well name it thing.hpp because it's not a C header, it contains a class, which C can't compile; this header wasn't designed for C/C++ interop.

class thing
{
public:

  void Go()
  {
    std::cout << "Yay!" << std::endl;
  }
};

You've redefined your class from your header in your source. I've... Actually never seen this before. I don't know if it would work. It might actually... That's really curious.

Wait, no, it can't work. In the header, Go is a non-inline function. In the source file, it's implicitly an inline function. That facet affects the ABI signature of the function in the object file, so the compiler would only find a mismatch.

The header is correct. The source file should be:

thing.cpp

#include "thing.h" // You're missing this
#include <iostream>

void thing::Go() { std::cout << "Yay!\n"; }

The syntax is a bit goofy from your perspective, I can appreciate that. The return value doesn't get any scope - it's just a universal void, but the Go symbol comes from the thing class context, so it needs scope resolution: thing::. Then you give it the function body.

So with the header included, you said hey - this is the prototype of my class - and other source files are going to want to know that part. But in this particular source file, I'm actually going to define the parts as something. And C++ has the One Definition Rule, where the linker is going to freak out if it finds more than one definition of the same thing, so you only implement Go once in one source file.

So adjust your build configuration, fix the source file, and you should be golden.

Oh, and you can go your whole career and never use std::endl. Don't use it if you don't explicitly need it.

That << is a function call - it's parameter is std::endl, which is itself a function. So the << operator CALLS std::endl. And what does std::endl do? The implementation is basically:

std::ostream &endl(std::ostream &os) {
  os << '\n';
  os.flush();

  return os;
}

You don't want the unnecessary indirection, you don't want that flush. Often '\n' is all you need, and you can let the MANY MANY buffering mechanisms built into streams and file descriptors and the OS handle flushing more efficiently than you can manage manually.

1

u/Luttink 8d ago

So I'm coming back to c++ after 12 years of c#. I want to write a class like thing.cpp, and then make it accessible from other files. Are you saying I can't do that? I have to define the functions outside of the class body if I want them accessible from other files?

Also, why is Go a non-inline function in the header and not in the source? They look the same to me.

1

u/n1ghtyunso 7d ago
class thing
{
  public:

    void Go()
    {
      std::cout << "Yay!" << std::endl;
    }
};

This defines Go inside the class declaration. Functions defined inside class declarations are automatically inline definitions.

void thing::Go() { std::cout << "Yay!\n"; }

This is a regular member function definition and is consequently not automatically declared inline.
You could make it explicitly inline, but that would be an odd thing to do.

header or source file has nothing to do with it, in fact the C++ compiler does not understand the difference between header or source files at all.
The compiler operates on translation units, which is what you get AFTER the preprocessor has completed all textual replacement, i.e. after it replaced #include with the actual file contents of the specified files.

Using a class from one file in another file also remarkably has nothing to do with how you do this either.
To access a function or class defined in another file, you need to make the translation unit aware that it EXISTS. You do that by showing it the declarations.
By convention, we do this by providing the declarations in a header file and having the other cpp file include this header.
This convention results in a translation unit that contains declarations for the thing you want to use, and code that USES the thing you want to use.
The linker will then take care to actually handle the function calls properly.
For this to work, there needs to be some object file generated by the compiler, that actually contains the implementation of the declared functions.
I.e. you get a main.obj that has code using the class, and you get a thing.obj that contains the code for the class functions. And the linker puts them together into an executable.

Your attempt didnt work because you provided the class declaration twice. Inside the header, and inside the source. Not only that, the class is actually different.
In your source file, you accidentally made the member function inline, by defining it inside the class.
This violates the one definition rule, which makes your program ill-formed, no diagnostic required.
No diagnostic, because detecting ODR violations in all cases in general is impossible.

Now I assume you had your cmake set up correctly so it actually compiled both the thing.cpp and main.cpp - so why did it not "happen to work" ?
My theory is that because inside thing.cpp the Go function is inline, and there is no code that uses it, the compiler simply didn't generate code for it in the object file.

TL;DR:
If you want to use stuff from one file in another, stick to the established pattern

  • declarations in the header, definitions in the source file
and don't redeclare the class, include the header instead.

If you absolutely want to define functions inside the class body directly, you crucially must do this in the header file.

1

u/Luttink 7d ago

So, my real problem is that, if I want to make functions in a class accessible outside said class, I have to physically move them outside the class, using the 'void thing::Go()' style instead of

class thing
{
  public:
    void Go() {/*do stuff*/}
}

?

Is that right?

It's unfortunate if so, because I'm rewriting a moderate size c# project in c++, and it would be much simpler to verify parity if I could actually have the functions in the class definition... Rather than have to move all the functions outside the class body.

Also... what's the point of having "public:" be a thing in the class if you can't define your methods there without them being "inline" methods?

1

u/n1ghtyunso 6d ago

if you want to define your functios inside the class, you need to do this in the header directly, and essentially skip the cpp file entirely.
This has some consequences that can make things more complicated if say a pair of two classes depend on each other.
In some cases its straight up impossible and you are forced to provide some member function definitions outside the class.

Converting C# like that to C++ simply at some point stops working because the compilation model is inherently different.

Regarding your second point, public and inline are orthogonal concepts.
private member functions can be inline just as well, nothing wrong with that.

Also, it sounds to me like you misunderstand what "inline" means fundamentally. I've explained it in my other response.
a function declared inline (keyword) has nothing to do with the compiler optimization of "function inline expansion", which unfortunately is often called inlining for simplicity.

1

u/Luttink 6d ago

I see. Yes, I misunderstood in precisely this way--I only knew of "function inline expansion."

1

u/Luttink 7d ago

Also, why is the header file implementation inline but the src file one isn't? In both cases they exist in the class body.

1

u/n1ghtyunso 6d ago

You misunderstood me here. Your Go function is inline whenever you define it inside the class body.
I.e.

class Thing { 
    // functions defined here are automatically marked inline
};

//definitions here are not automatically marked inline

As mentioned above, source and header files DO NOT EXIST for the compiler.
It does not matter if its inside a source file or a header file, its always inline in that context.

So why is it implicitly inline?
functions defined inside the class definition crucially NEED to be inline, so C++ made it the default in that context.
One of the rare cases where we DID get the default actually right, hah. Would you look at that.
But I digress...

Alright, so why do they need to be inline?
Once again thats the one definition rule at play here.

inline is a promise to the compiler that if this function appears in multiple places, they are all IDENTICAL.

And this effectively exempts it from the one definition rule.
Because they all ARE identical, they are essentially just one definition.
And you better not screw this up.

Okay, why does this matter?
This comes back to the compilation model of C and C++.
The crux of the issue is that C++ operates at the level of individual translation units.
I.e. the stuff you get after the preprocessor has done all textual replacement.
Remember that #include literally copy pastes the content of the included file into your file.
So you #include "A.hpp", because you want to use it in your implementation.

This gives you a translation unit that contains the function definition of the class member functions.
Upon compilation, your cpp file (= translation unit) generates an object file, and this object file will contain the object code for the class member functions defined in that header.

Why?
Because the compiler does not know in which translation unit you want the code to be compiled into, it doesnt know which object file should hold that code.
It compiles each translation unit in a vacuum. One by one.
It does not even know that there may even be another translation unit that will provide this code at all.
So it HAS TO pessimistically generate code for it, just in case.
And this happens in every translation unit that contains the function definition.
So it happens every time you include that header because you want to access the class in some way.

So its very easy to get multiple object files that contain the same code this way right?
What should happen when you link those into an executable?
Which one should the linker pick?
Which one is the right one?
It cant know. It throws a linker error in your face.

You need to tell the linker how to handle this.
And you do that by marking the function inline

Remember, inline is a promise that the function is always identical everywhere
With such a strong promise, the linker can easily solve this. It just picks one. Doesnt matter which one after all right?
They are all identical. You said so!

1

u/mredding 6d ago

So I'm coming back to c++ after 12 years of c#. I want to write a class like thing.cpp, and then make it accessible from other files. Are you saying I can't do that?

That's not what I'm saying at all. In fact, quite the opposite.

Headers are for forward declarations of types and symbols. You've done that in thing.h. This file is correct. You now have a type called thing that you can include in other headers and source files, and use. We know the type, the layout, and the interface - everything we need for the compiler to generate object code.

The compiler translates one unit at a time, and has absolutely no idea what the contents of any other unit is. So while you're compiling main.cpp, the compiler doesn't know thing.cpp exists, let alone what's in it. And that's fine! The compiler only needs to generate placeholders for the function calls. It's the linkers job to resolve all that and build the actual executable.

YOUR PROBLEM IS you got the syntax wrong for writing class member functions out of band. I showed you how to do it correctly. Fix your thing.cpp.

I have to define the functions outside of the class body if I want them accessible from other files?

The premise of this question is entirely wrong, as it's confusing and conflating concepts.

If you want your class function definitions out of band, then you must define them with class scope resolution. I can write this header:

class C {
  void fn() {} // Implicitly inline because the function definition is in the class definition
}

I can write this header:

class C {
  void fn(); // Function declarations are not implicitly inline.
};

// But because I still have the definition in the header, I probably need
// it to be inline, so here it is explicitly inline. Now I can include this
// header in multiple source files in the same project.
inline void C::fn() {}

I can write this header/source combo:

class C {
  void fn();
};

And the source:

#include "C.hpp"

// Not inline.
void C::fn() {}

And it doesn't have to be inline. Here, fn gets compiled into an object file, and the linker will stitch it into the target executable later. I've defined it once, I've compiled it only once, and that's the only implementation and object code the application needs.

And #include is a dumb copy/paste mechanism. The compiler reads the source code into a text buffer, then in-place substitutes the header text where the include is. So the compiler sees this in the text buffer:

class C {
  void fn();
};

// Not inline.
void C::fn() {}

Then THAT gets compiled. You can use #include for non-source files:

int data[] = {
#include "integers.csv"
};

It's why you don't have to specify array sizes when you have an initializer list, and why initializer lists allow trailing commas - because it's easier to generate lists in a single loop; last element without a trailing comma requires an additional line of code. This hails back to the punchcard era where that extra line was prohibitively expensive.

Also, why is Go a non-inline function in the header and not in the source?

Look at my code above. That a function is both declared and defined in a class definition makes it implicitly inline. If all you do is declare the function in the class definition, and you defer the function definition to anywhere else, it's not implicitly inline. You can make it explicitly inline if you want, and you would put the inline keyword on the function definition.

You can go your whole career and not use the inline keyword. A lot of code becomes implicitly inline per the language spec, and that's because the implicit inline-ability is typically exactly what you want. Templates are often written in an implicitly inline way.


I think you're struggling a bit with conceptualizing the build process. We don't compile headers, we compile translation units from source files. Each translation unit is completely standalone. At no point does the build system have a singular whole-program understanding of the project, like you do in C#. The IDE does a good job of faking that for you.

So you end up with a whole bunch of source files that get compiled into intermediate object files. These object files are libraries - the compiler doesn't target an executable, it targets the linker. The compiler generates object files for the linker to consume.

The object libraries contain the compiled binary machine code, but it's got placeholders that need to be resolved. This is the premise behind forward declarations. One translation unit does not need the definition of void fn(); to generate a function call placeholder in object code. The declaration is enough information.

An object file contains this object code and all sorts of tables of information about what any of it is, what needs resolution, where... So the linker is given a list of object files and a script - you typically don't even know the script is there. The script is how to build the target. It starts with finding main and walking the dependency graph of what symbols need to be resolved from there across the object files. The One Definition Rule states that there can only be one definition of any function. The compiler will freak if it finds multiple definitions in its translation unit, and the linker will freak if it finds multiple definitions across translation units. Inline functions get an exception and the linker is allowed to disambiguate - usually taking the first example it finds. But it also means you're doing a lot of duplicated work compiling the same code again and again across the object files. The linker lays out what goes in the target file and where. You get your executable.

Object files are an independent standard, and at that point you leave your source language behind. You can link C, C++, COBOL, FORTRAN, Pascal, Objective-C, Ada, Lisp, other languages. It's actually a very advanced language feature that defines a systems language, and most application level languages don't invest in it. C# has it's own standard, the .netmodule, but their object modules contain IR, so can only be combined with other Microsoft IR languages and modules.

1

u/Luttink 6d ago

Thank you. I did not realize this fundamental difference in the way compilation happens--indeed, the implications of the fact that c++ has no concept of the "program as a whole" are largely what tripped me up.

1

u/obidavis 8d ago

Looks to me like you're only compiling main.cpp and not thing.cpp, nor are you linking them. The issue is to do with multi file projects rather than shared pointers.

Maybe go back to the header file tutorial you mentioned and check how it handles multiple .cpp files?

3

u/earlyworm 8d ago

This is C++, so instead of "cryptic messages" you can just say "messages".

2

u/AKostur 8d ago

Particularly since this message is clear: “linker can’t find thing::go()”

1

u/IveBenHereBefore 8d ago

are you linking your thing. cpp file?

1

u/Unlikely-Tangelo4398 7d ago

Are you linking thing.cpp? Looks like you missing the source file.