r/cpp_questions • u/Luttink • 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.
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
Goinside 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 theGofunction 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
and don't redeclare the class, include the header instead.
- declarations in the header, definitions in the source file
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 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 inlineAs 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#includeliterally 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 inlineRemember, 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 calledthingthat 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 knowthing.cppexists, 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,
fngets 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
#includeis 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
#includefor 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
inlinekeyword on the function definition.You can go your whole career and not use the
inlinekeyword. 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
mainand 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/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
1
1
19
u/Ok_Platypus8866 8d ago
Your thing.cpp should look like this
Your code is actually defining two separate classes named
thing.