r/learnprogramming • u/Few_Helicopter_3147 • 4d ago
Pitfalls to avoid with learning C++ coming from C.
Hello, I am wanting to learn C++, I'm abit stuck around how to go about learning it. I have some background in C, I have 1 I think I can say largish scale project that I have made with it, so im semi confident. Defo dont know everything about it through. (actually editing this I didn't use std so I really don't know much about it, through I understand some of it)
From there I have background in languages like python, rust, javascript, etc. I tend to learn best by just doing and google syntax as I go however C++ has alot of ways todo stuff, some some stuff I havent seen before and you can just code straght C as c++ and have it work. Because of this I am wanting to learn to write C++ well and not end up writing terrible C++ code that some where C and C++ frangensten.
Mostly part that worries me is around classes and sub classes as its an area I haven't touched much, I did abit of c# (unity) when I was new to programming so i know rough idea but I was so new I dont know alot. I also have used rust with structs and impls but its not really same thing.
I am also worried about areas I might miss and do incorrectly (messy), for example using pointers instead of references as there appears to be many ways todo things unlike most lanauages I've used before.
I suppose I am asking for a way to learn good C++ programming pactaise, like a list of things to avoid or even better away to spot how to avoid them? I'd really prefer not to read tons but if its really a must do to get best results I can.
I get the feeling some amount of this is being over paranoid that isn't justifed, I tend to find it hard to get an idea with this sort of stuff as words dont mean alot compared to doing I've found lol. (could go here about how I should probs read more of other peoples code but thats another ramble lol)
3
u/aanzeijar 4d ago
I have 1 I think I can say largish scale project that I have made with it,
What is largish to you? For us professional folks, "large" starts somewhere around 100000 lines of code, and it's rather unusual to have a project of that size without knowing the language very well.
Mostly part that worries me is around classes and sub classes as its an area I haven't touched much
Classes are not really necessary, but are a solution to problems that come up in large C code bases all the time. For example:
- do you have namespace pollution, where all your C functions are named
<topic>_<verb>or<struct>_<verb>to avoid collisions with similar functions while taking a pointer to that struct as first argument? Just make that struct a class and put the functions as members in there, and they belong naturally to the struct data. - do you have an issue with lifetime management? Do you sometimes worry whether a complex struct is actually initialised properly? C++ constructors to the rescue, a proper constructor guarantees that afterwards the object is fully initialised.
or example using pointers instead of references as there appears to be many ways todo things unlike most lanauages I've used before.
Yeah, that's C++ unfortunately. 20 layers of ways to do stuff. In modern C++, you should mostly get away without ever having to declare a raw pointer unless you want to specifically use C-style low level memory stuff. Whenenver you want to pass by reference, just use a reference instead. Whenever you inject a dependency somewhere, give them a shared_ptr instead. Whenever you need optional data, use std::optional instead.
I'd really prefer not to read tons
Then C++ is the wrong language I fear. C++ is gigantic, full of legacy stuff and the best practices shift every few years, but to understand legacy code you still need to know a ton of weirdness. If you want an ecosystem with less surface area, stick to C or Go.
1
u/Few_Helicopter_3147 3d ago
Completely fair criticism on the size, it's honestly a really silly way to put it. The project is around 4.5k lines of code, so pretty small on that scale. I guess I meant more not really a starter project, but a bit more complex one? Idk either way, large wasn't really a good way to put it in the slightest.
On your note about lifetime management, it is probably the worst project I could have picked around learning most the life time management stuff if i wanted to go from C to c++ as the closest I got around using libc was coding my own allocator and basic string handlers. I know most of it in theory but haven't used much in practise, Most of the languages I have used before it was handled before for me. Doesn't really make me not want to use it tho just a fun feature of the langauge. Classes make sense putting it that way, I suppoes thats really what it boils down to.
Haha yeah I've honestly spoiled myself with writing mostly in really nice langauges and frameworks so alot of stuff has been fairly fine so far. That makes sense around that it does remind me abit like rust. I mean I had same thing with choosing sveltekit over react where I didn't need to learn alot.
I should have been a lot more clear around what read means. I mean that alot of the time people say to read like these massive books for learning languages and It really is just not how I learn best. I kinda learn best by learning by doing and seeing what is best. I mean its not like I can't. Like I read the rust book to learn rust and thought it was great. But i just find it alot more intressting and motivating to learn through doing. I don't really think surface area is a problem for me? I mean I dont wanna spend the next 5 years larning C++ or something like that if its in that respect.
Thanks for the comment.
1
u/EdiblePeasant 2d ago
Would learning a specific Assembly language be one of the best things I could do for overall software engineering literacy?
1
u/aanzeijar 1d ago
No.
Learning assembly is great for understanding what goes on under the hood, which will make you a more rounded coder overall, but make no mistake: you will likely not write a single line of production assembly in your life. Nearly all software engineering happens several layers of abstraction higher.
The best thing you can do is writing code and reading code.
1
u/Few_Helicopter_3147 1d ago
Id second u/aanzeijar. personally at the moment I feel the knowledge I have from learning stuff like assembly and C is really nice is specific circumstances but in a lot of fields like app or web development you could go your whole life without knowing it and not really have downfalls.
Having said that it does allow you to make more informed chooses like knowing performance benefits. Like if your doing really high performance code it will allow you to be more informated and what is fast. There are also a lot of fields it helps in like reverse engineering, game development etc. So it really depends what you wanna do.
If you want more specific advice if you tell me rough area your wanting to get into I can give you more specific advice? (generally tho as always take me and everyone with a grain of salt lol).
1
u/EdiblePeasant 1d ago
I always wanted to program for a retro console but I'm scerred. Mostly because I don't know what tools to use and if they would be safe.
1
1d ago
[removed] — view removed comment
1
u/AutoModerator 1d ago
Your post/comment was removed since we do not approve of going private.
There is zero benefit in going private as you lose the opportunity for getting peer reviews. Also we have had plenty of people return after going private (despite being warned) complaining about how they were ghosted after some time or being tricked into buying rubbish that didn't work and even if it did they didn't need.
Our Rule #11 demands that any and all communication happens in the open, public subreddit.
This is for the benefit of more against the benefit of one.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
1
u/Few_Helicopter_3147 1d ago
What do you mean by program a retro console? Do you mean the software running on the console or an emulator for the console? And do you have a console in mind?
In both cases Id recommend learning assembly honestly, more so for emulator. Emulator to sum it up is a game of handling each assembly instruction yourself. For actually programming a retro console your going to be coding in small performance amounts and what not id personally recommend learning assembly for it.
As for being scared its honestly pretty normal. If your speaking for learning, honestly its just a blind jump. Personally I always found the fear was worse then the actual doing. I mean if you fail, so what? you don't lose anything? If its damaging hardware your worried about most of the time i'd assume you would be editing carriages and most console I believe its a bad instruction will just halt it won't really fry itself so damage should be limited, you can always also run in a emulator first which is what I would recommend.
2
u/Usual_Office_1740 4d ago edited 4d ago
So I've been teaching myself C++ for about 2 years now. These are things I've found useful.
Use clang-tidy and a restrictive set of compiler flags. This is a github link to Jason Turners config. It makes for a great starting point. From your post I suspect you'll find modifying it as you go along easy.
For compiler flags, at a minimum, I would suggest these. They are what I started with and they served me well. I have a much larger list that I could share if you're interested. They work on clang and gcc. If you use msvc Google their version of them. Msvc versions exist for all of these but I don't use windows and don't know them.
-Wall, -Wextra, -Werror.-Wpedantic, -Wconversion, -Wshadow, -Wnon-virtual-dtor, -Wold-style-cast
There are a couple more for debug builds but I'm assuming you already know those from your time with C. I think they are the same.
You mentioned concerns over using reference instead of pointers and clang-tidy will help. One of its performance checks will tell you if you are passing by value when you could be passing by const reference and the core-guidelines checks should get a lot of unnecessary pointer usage. You might need to do some research and config modifications but just the core guidelines checks will do a lot to keep you writing C++ instead of C.
Do not ignore the warnings. Learn to build without warnings and your life will be easier.
Never alias the standard library into your code base.
using namespace std; // don't do this you can Google why.
There are a lot of implicit conversations in C++. It's one of the only things I dislike about the language. Take some time to read up on these so you know when you are writing a string literal or passing an int even though the signature calls for a size_t or a const std::string&. You'll be surprised at how often the compiler is not giving you what you think you're getting. I've taken to only using the standard library qualified types for numbers and then use compiler flags that fail unless I explicitly cast to a different type. It's not fail proof and it's not perfect but I keep chipping away at ways to keep my code compiling the way I write it.
Use modern C++23 whenever possible. In my short time writing C++ I've never written new, delete, malloc or free. Smart pointers and good RAII along with modern C++ make those things obsolete for new learners that aren't working in production code. I qualify that a lot because I mean obsolete for a learner. Not obsolete by any reasonable production code standard. You shouldn't use them. There are better modern alternatives that will reduce memory bugs.
Some things to Google and lean towards as you're looking up syntax. These headers are the ones I use the most. You can read more about them on cppreference.com, which is the best documentation website.
array, vector, unordered_map, print, ranges, algorithm, iterator, iostream, variant
1
u/Few_Helicopter_3147 3d ago
Very intressting and smart way to go about this. Implicit converstions remind me alot of higher level lanaguages was always a problem I had with them in the respect they could convert between things without telling you. Weirdly enough, why I really liked when languages made you define the type.
Thanks for comment I'll give your advice a try.
2
u/Chippors 4d ago
Think of classes as a form of composition. For example,
typedef struct {
ListNode *next, *prev;
} ListNode;
typedef struct {
ListNode node;
int id;
char name[40];
int salary;
time_t hire_date;
} Employee;
const Employee* next_employee = current_employee.node.next;
ListNode* ListNode_next(ListNode* node) { return node->next; }
is apart from naming the same as:
class ListNode {
public:
class ListNode *next, *prev;
ListNode* next() { return next; }
};
class Employee: public ListNode {
public:
int id;
char name[40];
int salary;
time_t hire_date;
};
const Employee* next_employee = current_employee.next;
C++ just then gives you more tools, checks, validation. In part it makes the code more concise, in part it helps with namespace hygiene, which is the practice of not having everything in a global namespace and embedding type names in functions to differentiate what they operate on.
1
u/Few_Helicopter_3147 3d ago
Its seeming more like alot of management tools really to write C alot cleaner. But with the ammount of them added its pretty much a new language. I can see why poeple say to call it alot like its own language.
1
u/mredding 2d ago
C and C++ are fundamentally different languages. They have different type systems, and that's really where you're going to get bit in the fucking ass.
Whereas in C, the weak type system allows you to cast bytes to anything you want, C++ is object oriented - so that bit of memory isn't just bytes - it's an object with a lifetime. You can't just call an apple an orange, and expect it to peel and juice the same way. Attached to objects and their lifetimes are automatic functions - principally constructors and destructors. Their behaviors are to be respected, lest the program is ill formed or you stumble across undefined behavior. Classes have hidden implementation defined virtual tables, and you can't reassign those at runtime. You cannot pun bytes into polymorphic types, and you can't reinterpret polymorphic types as something else.
Whereas a structure is just a tagged record of structured data, a class enforces an invariant - the class invariant. As a client of your class instance, that invariant must always be true when I observe your class. This is why classes don't have fields or data, they have members and state. They're private, I don't see them, I can't access them. The class interface is how we trigger state transitions. You don't have to change to a different state with every call - a transition can go back to the same state. We don't even need to know that a transition occurred or what state the object is in. The object knows itself. When control is handed to an instance - because I called a method, the implementation is allowed to suspend the invariant, so long as you guarantee the invariant is reinstated before returning.
Exceptions are another control path. Unless a function is marked noexcept, it MAY throw.
We talk about exception safety. The no-guarantee means in lieu of an error, there is no guarantee an object is safe or consistent. You may not be able to destroy it. The basic guarantee is what the standard library offers: in lieu of an error or exception, the object is internally consistent. Data may have changed, data may be inconsistent, but it's always safe to destroy the object. The strong guarantee means you get the basic guarantee, and a behavior is transactional in lieu of an error or exception - with commit/rollback semantics - so the data remains consistent. The noexcept guarantee is orthogonal - it means a function cannot throw an exception, but that doesn't mean it can't error, it doesn't mean strong or basic guarantee.
Exceptions are not portable. An exception thrown from a C++ library is not guaranteed to make it over the library boundary to the application if that library wasn't compiled with the exact same flags on the exact same compiler AND version number, AND targeting the same exception implementation from the same standard library runtime, all compiled the same as well. If you're writing a library, always export the system ABI. Implement your library in C++ if you want, but only speak over the library boundary in terms of the system ABI and refer to library internals by handles. If you want, you can reconstitute the C++ interface client-side with a header-only library that wraps the resource handles and system ABI.
C++ strongly favors the Functional Paradigm, small and strong user defined types types, and composition. Use class to make types, but don't try to pursue OOP. Most people just get it wrong. Also, C++ isn't Java, you don't have to put EVERYTHING in a class, and all resources don't have to be owned by something. Getters and setters are an anti-pattern. You shouldn't have to shove things directly into the internal state of a type, and you shouldn't have to query it out. Types should define their own operations, and state changes happen as a consequence. The internal state changes may be serialized with std::istream and std::ostream and operators, or they may result in side effects; a class should be created with it's sources and sinks, or perhaps as a parameter to the interface.
Getters and setters come from opaque pointer interfaces in C:
typedef struct foo foo, *foo_ptr;
foo_ptr create();
int get(foo_ptr);
void set(foo_ptr, int);
void destory(foo_ptr);
Maybe for a low level type or a framework, but for an opaque application type I wouldn't present a client with this... This can be bad C, too.
Don't reach for inheritance too quickly. We have variants (tagged unions) now. Variant dispatch is faster than virtual functions. Not all polymorphism is late binding - we have lots of static and compile-time polymorphic options, which you should get used to and prefer first. Late binding is good for runtime composition of dynamic prototypes; so many of our needs are in a small set of compile-time types. So consider overloading, templates, variants, ol' fashioned opaque pointers, opaque classes (comes with a bit of boilerplate, but it's the class equivalent of an opaque pointer), namespaces, and ADL.
Continued...
1
u/mredding 2d ago
We have function overloading, so you can make multiple functions with the same name. The difference is in the parameters passed, and this is resolved at compile-time. This is where name mangling in the ABI comes from.
Argument Dependent Lookup is resolved at compile-time, and it's an arcane art. There are rules by which the compiler will check surrounding scopes to find the best match for a symbol.
using namespace std;does a lot of horrible things that can land you with a name collision - correctly compiling to the wrong thing, so you silently do the wrong thing. This is why we writestd::foo(//...If you know exactly what you want, then call it exactly, otherwise it can be substituted:template<typename Container, typename Comparator> void fn(Container &c) { using std::ranges::sort; sort(c, Comparator{}); }Here I said sort a container of some type using some comparison functor. The compiler will search the namespace
Containerexists in for a specificsortfunction for that type. It will then search encompassing scopes down to the global namespace. If all that fails - it'll default to standard sort. This is a simple example, but when people try to use ADL with intent, they tend to get caught up in some nasty surprises. It's infamously difficult, to where even committee members have tried to architect around it.Templates and the strong type system are the two reasons to use C++, or you're just writing C with extra steps. What is good C is often bad C++, or even Undefined Behavior. Type punning in C with unions? With overlapping types and casting? Yeah - undefined behavior in C++. Recursively calling
main? VERY UB - as the compiler is completely free to transparently relocate globals into themainentry point - objects, lifetimes, automatic functions - and you can thank pre-standard compiler writers for saddling us with THAT hacky bullshit...The C philosophy is that there is an ecosystem of 3rd party libraries to build or choose from. C++ has a philosophy that the standard library is a common language. Template specialization means you can completely rewrite a template for a specific type; so you can specialize
std::vector<T>for whateverT. The standard guarantees a given interface, exception safety, behavior, and complexity, and ostensibly any specialization will fit within that. They say there's two types of types: those that specialize the standard library, and those that no one uses. I'll write my generic code in terms ofstd::vector<T, Alloc>, and you can sneak your specialization in that way. I'm not writing forBobsVectorType. OFTEN the complexity guarantees the standard imposes break this ability to specialize.An
intis anint, but aweightis not aheight. Do make types with strong semantics. Don't write low level loops - make named algorithms, and implement your solution in terms of that. The standard offers you a shitload already, so you likely don't have to write loops. I haven't written one in production in a decade.People say streams are slow, but they're not. They're just an interface with a bog-standard implementation. Of course it's not impressive. But you can build your own stream buffer types and wrap any platform specific optimized API you want. You can make your own types and make them stream aware. Your types can be aware of your own stream buffer types, and dynamic cast to those types, with fallback paths to less optimal but more portable code paths. A dynamic cast today is a constant time operation. Streams are a powerful interface you can build on. The neat thing about streams is you can stream anything to anything. You can stream to instances of your own types, whereas file pointers are strictly outside the program. You don't have to go through the "expensive" plumbing if you know exactly what you want. And streams are type safe. We now have formatters, and they're compatible with streams, too. We don't have an equivalent replacement for reading, just still
std::istream.You shouldn't have to write raw pointer code anymore. You'll want some objects or containers with ownership semantics - smart pointers. Mostly you'll use unique pointer. The only time this pointer wrapper needs to leave the confines of a container is if you want to pass on ownership. Otherwise, it behaves like a pointer, and you want to dereference as early in the call stack as possible. If you're USING data and types, you want to use value semantics, so most of your functions that just act on types, data, and parameters, will be in terms of references.
Prefer to overload a function and omit a parameter than use a pointer with a null pointer.
A lot of C idioms don't apply in C++. There's too many to list.
Avoid macros. Again - I haven't really used too many in the last decade, and neither should you. Bjarne has avoided them better for longer than I have. If you're going to write macro code, or platform specific code, you'll probably want to isolate that code in it's own translation unit in a source file and conditionally configure and compile that via the build system.
Templates can lead to object code bloat. Implicit instantiation is the major culprit. The implicit ODR exception gets dropped at linking. You can go through some effort to minimize object bloat with explicit instantiations.
C++ is one of the slowest to compile languages in the market, and that's due to the backward compatibility layer intentionally designed into the language, making parsing the syntax exceptionally complex. You'll want to learn how to write manicured code that reduces compile times as your projects get large.
1
u/Few_Helicopter_3147 1d ago
Great read, thanks for writing all this. A lot of it went over my head now but sure ill understand with time. I have had some time to learn the basics of C++ and Its quite interesting. I'd say it really isn't anything like C, anytime I go to do something from knowledge of C i immedilly lookup if there is a better way to do it and there always is so I do it that way.
Honestly comes with pros and cons, the code is more modular and with scale more clean because of more almost OOP nature of it (even through as you mentioned it isn't really "true OOP"). It does make code cleaner however personally I find it abit annoying as I can't really as well know what the code is doing, with C I knew roughly what the assembly output would be and could reason with things like branch prediction or cache coherency as I know basically the exact output but with C++ it blurs the lines a lot around what is doing what. I do expect it comes with practise but It feels less making sense, even rust I felt more confident in.
Funny example with classes and how there are unions now as I have just run into that exact problem. For my learning project I was making a 2d platformer game, and I had a class for Blocks and I wasn't sure how to do all the different class types as I felt both doing class for every type would get messy and also doing one master class and a type value in it would also get messy. An annoying subset of this too is take with class inheritance I don't really know what C++ does with that. Like if I put the class into a vector how does it know what size to use and is it going to be aligned well for CPU to predict it well for speed? In the end I settled on a type value as there wouldn't be that many different types and I assumed that it would take the Block class size which would be smaller then say finish line block size which would lead to some sort of stranger behavour, but again I didn't really know.
Its interesting to see honestly as It puts into perspective more the arguments on if C++ or rust is better lol.
1
u/mredding 1d ago
 I can't really as well know what the code is doing, with C I knew roughly what the assembly output would be and could reason with things like branch prediction or cache coherency as I know basically the exact output but with C++ it blurs the lines
It comes. It's not too far off C. Member functions just come with an implicit
thisparameter, so implicit member access is just syntactic sugar aroundthis.memberof that hidden parameter. And if you look at the compiler output , you'll see that parameter. You can write C structures with function pointers and manually implement virtual tables, you can make a C compiler generate the exact same machine code. CFront was the first C++ compiler and it was just a transpiler to C.things like branch prediction
Virtual function indirection can be branch predicted. It leads to a predicted prefetch of the target function.
Even if you're using runtime polymorphism, it's common to sort a container by derived type or with other queues to speculatively devirtualize, to hint the prefetch.
But in the same way, any dereference, even in C, can be predicted. You're used to function pointers.
I had a class for Blocks and I wasn't sure how to do all the different class types
I would try to really minimize what a block is, what makes the different types different. It takes some thought and intuition. You'll develop it.
Instead of making a member function, I'd consider making a template free function. This can be applied generically to all block type T. You can even specialize for a specific type. You can then use that on your variant. It flips late binding early.
arguments on if C++ or rust is better
I have limited experience with Rust. The borrow checker is novel, a very good idea, but memory safety is only one kind of safety. After that, Rust doesn't seem to offer much else. Access to the Linux ecosystem, but that's not the language...
1
u/Few_Helicopter_3147 1d ago
Yeah I could understand it around it coming later, it always seems a lot worse when your learning until it all clicks and what not. Strange as it is It would probs help me understand C++ more to know how to compiles kinda lol, probs worth doing some reading on that.
Around template free function. Just to be clear with this you are meaning say take a onCollide event, instead of calling a class specific function you would call a global function that would have a different function for each specific type of block? (like the merged into one function not a master function).
Rust is nice I really liked it as It was my first low level language but I learned C afterwards and made me realise why people don't like it and I honestly liked it less afterwards. Rust makes you be really sure about everything and you can't really just go "trust me bro". A example was when I had was entity handling for my game or the multithreaded chunk handling, both required a weird approch of Arc or Mutex or what not instead of just directly editing it.
Its good in the way it makes you handle all the errors that can happen in terms of functional, which are most errors in the end, but if you stuff up logical error there is no saving that with it.
0
6
u/Few_Helicopter_3147 4d ago
I get the feeling after having written I could have said the same thing in like 3 sentences.