Hope you all enjoyed the video! Have a go at the exercises and implementing some of the features we didn't get to in this video (eg. copy/move constructor + operators for the Vector class). And finally don't forget that the first 1000 people who click the link will get 2 free months of Skillshare Premium: skl.sh/thecherno0820
I am really happy with this video! While you provided the solution, I wish you would have also highlighted the issue of the old allocation: it constructs! Imagine if your class had a Sleep in the constructor, or more realistically, a heap allocation. With out operator new, it would slow you down substantially whenever you wanted to resize your vector. Especially over thousands of items.
Hello! Thank you for the video and for your work! I found that the... outcome of this code depends strongly on the compiler. May I humbly suggest a topic for a video about compilers in more details, which one do you use with what flags and what are the pitfalls or gcc, g++, clang, calng++. I am working in Ubuntu and I found that the only compiler that works for this project is "rustc" that I've never heard of before :)
Learning data structures in c++ makes it much easier to understand and do in other languages. C++ was the language used in my comp sci program for intro to computer sci and for data structures and algorithms and now I’m doing data structures and algorithms in JavaScript preparing for interviews and that foundation in c++ helps a ton. If anyone is a student and has to do c++ in school, don’t be overwhelmed. Trust the process, work hard, and you’ll do just find. Cherno clutch yet again with perfect quality content!
About 2 years ago I really became intrigued by stl. The vector class was the first one I tried recreating. And failed miserable. How ever I have redone this exercise about 20 times since. And I can write a vector class with the majority of the features one would want in about 1 day. I encourage everyone to do this exercise every once in a while. You always learn somethimg new.
You still have a bug in ReAlloc function in line 95: newData[i] = std::move(...); - you can't use assignment operator here (either move or copy) because newData[i] is uninitialized memory - it is not the object of type T and by calling operator= (again move or not) you treat it as if it is object of type T. So instead of that you should use placement new once again so change that line to: new (&newData[i]) T(std::move(m_Data[i]));
Thank you man!!!! I am looking for exactly the fix for this issue! The code does not work for std::string, it crashes exactly at this line!! Thank you man I appreciate it!
Yes, I've noticed that too. I'm mostly going by myself but my ReAllocate function is very similar to this. My constructor for a class allocates some memory but the constructor is not called so when assigning (copy is called) the data is just leftover memory.
hey newBlock is of type T* so I replaced the code you gave, but now its showing Buffer overrun while writing to 'newBlock' error 'Vector3::Vector3(const Vector3 &)': attempting to reference a deleted function
It is initialized. When you create your array as "new T[size]" it initializes every element with default constructor. Which is a problem, because what if your value_type doesn't have default constructor? To create uninitialized array you need to use allocator. I also would suggest to use std::construct_at instead of placement new operator. It does the same thing, but more descriptive and easier for an eye.
40:13 Constructors are being called there if you do a cout in your constructors you will see them print. But I assume the point being that they don't need to be called so thats why we switch to an operator new.
Wow, that gotcha moment at the end was so inconspicuous that I'm a bit shaky in writing efficient C++ code and debugging it. May this series never end for the good of me!
@@sandeepkalluri Essentially, delete operator does 2 things: call dtor, and then free memory. delete[] call dtor of each of its elements, then free memory. The problem is, if we manually call dtor of some item, it will in fact delete its members memory(int* in our Vector3 example), but the Vector3 instance is still physically present in the contiguous array of our Vector instance. That means that if we call the Vector delete[] method, ie when the Vector instance is out of scope in this case, Destructor of each item may be called Twice ! The problematic items here are the one cleared or popped back. When delete[] iterates through items to call dtor and free, it will not stop at our m_Size defined value, but out Clear implem does. If you want to check stuff by yourself, just simply emplace and immediatly pop some object from Vector in debug mode. You can clearly see that in the memory our object is still living after pop. Destructor of an instance manages its member destruction, not the object itself. We have a similar concern with the new operator(although it doesn't cause crashes): it allocate memory, then call ctor. In case of re allocating resource (or "reserving"), we want to make sure we have enough space for our future item to be added. We do not want to actually create default objects waiting to be overwritten by future actual pushed back ones. That is why we use this syntax: ::operator new which only allocate UNINITIALIZED memory, ie memory we own but may contain unpredictable garbage. Hope this helps.
@@sandeepkalluri new calls the allocator to allocate memory, with size of the type you passed in, and then call the constructor to create an object on that memory allocated delete calls the destructor first, and then calls the deallocator to deallocate memory the ::operator new only allocates memory, and it does not construct anything, the memory is now yours, but it has nothing in it, to construct an object of type T in that memory, you have to do something called a placement new new(address) ctor(data) so the normal new operator that you would normally use, it does two things automatically, allocate, then construct in this video, what you're doing is you do those two things by your own the same goes with ::operator delete(ptr, size * sizeof(T)) it only deallocates, and does NOT call the destructor so what the normal delete would do is to call destructor, then call the ::operator delete behind the scenes, the deallocate since in this video, you have a lot of parts that you call the destructor explicitly, and then finally you call the normal delete, which calls the destructor again, despite that it has already been called at some other part of the code if you're sure about calling the destructor exactly where and when you wanted, then all you need is the deallocator And since you can't call ::operator delete on the normal new, you have to also do the two task: allocate by using ::operator new and placement new manually, just so the ::operator delete can work, deallocates what ::operator new has allocated
After all the fame, the fact that you keep this C++ series for dummies like me going, is amazing! The game engine series is way over my head and the reaction videos are not really my thing.. but I do hope that you are getting all the compen$ation you want because you deserve it... kuddos mate
Already figured out how. I have just write begin() and end() methods which returns something acts like iterator, but i done this in custom string class, in vector class it should work the same way.
42:48 Is this assignment valid in 96 line (underlined by the way) ? newBlock[i] = std::move(m_Data[i]) It is assignment xvalue object to raw memory. Is it ok ? Shouldn't be placement new again something like that ? new(&newBlock[i]) T(std::move(m_Data[i])) If yes, then maybe your code has UB but did not crash.
I think it doesn't matter how the memory where the object gets copied looks like. It's just important that there is memory that can be written. The move constructors initialises the memory in the raw area how it needs. Every time a c++ class gets allocated the constructor writes data into raw memory. The member functions and stuff gets not stored in the allocated memory, if that was your doubt. The allocated memory just holds data and on construction the data is always raw. Hope this helps a bit.
you are correct. the implementation in the video can mess up if you create a vector of a class with virtual methods, since the vtable pointer of the object won't be initialized.
I have added more constructors which takes arguments as initializer_list : 1. Vector(const std::initializer_list&); 2. Vector(std::initializer_list&&); Then I have created a Student class and created three objects of it s1,s2,s3 And then I try to do following: Vectorstudents = { s1, s2, s3}; Vectornumbers = { 1,2,3,4,5,6,7,8,9 }; So as I am passing s1,s2,s3 as "lvalues" for students and 1,2,3,... as "rvalues" for numbers. But in both of cases it is calling 2nd constructor. So why Vectorstudents also calling 2nd constructor? Can you help me ?
I think it's because in both cases you're still initializing the vector with temporary instance of initializer_list (so r-value). If you want to call the l-value constructor, you'd have to do something like: std::initializer_list list = { s1,s2,s3 }; Vector students = list;
Very cool! I think I “know it all already” but usually learn something new in these! I’m writing my own containers now for my personal engine project and great to get some ideas from The Cherno 😀
This video inspired me to create a sort of a mish-mash of two data types together, basically a compromise b/w a linked-list and a vector. What if you have moderate-sized arrays that link to each other? Array[10]->Array[10]->Array[10]->... and so on? i) This way, you don't have to make as many hops for reaching an element. MishMash[35] is actually just two hops away, rather than 35 hops! ii) You still have to allocate a new buffer for every 10 inserts (this could also grow exponentially), but you don't need to copy the old buffer. I feel like this data type can be used for vectors that dramatically alter in size frequently.
an optimization that i use in my game engine array class, is using type traits to check if T is a POD, and if so using memcpy (not memmove for performance) to copy elements instead of using std move. compilers may notice this, but i added it just in case
42:06 line 98-99 calling the destructor on m_Size works if newCapacity >= m_Size. But if newCapacity < m_Size, you might miss out destructing some objects in m_Data?
Stack allocated vector that is actually just a single node in a linked list. Every so often you get a slower step, but I am so intrigued to try implementing it
I tried to figure out an function to do the doubling of the capacity initially, but heavily dampening as the capacity grows, but nothing was really that satisfying. Like it'd always still grow quite a lot once it reached like 500. Which is obviously quite unnecessary as you'll take forever to fill it even if you had that much more data. Or it wouldn't grow a lot in the beginning which is also the opposite of what you'd want to. Without doing if checks for the size, that'd also be the opposite of what we want, building processing cost trying to save it.
Can you make a video explaining your coding style and naming conventions? I think it would be cool to see a little video about why you prefer PascalCase and prefixes on variables (like m_ and s_) and what are your tips for choosing naming and coding conventions. It would also be cool if you could talk about your C# conventions as well (as far as I know, the m_ prefix isn't very popular in C#, so I think it would be cool to know if you stick to it and why).
He once said that he used to use camelCase back when we was doing java but that after sometime at EA he got so much used to PascaCase that he cant use camelCase anymore (sorry bout my english)
Choose what do you personally prefer (or make a consensus within the team you are working with). Some IDEs (e.g. CLion or VS w/ReSharper) allow to set preferred naming conventions in Settings and they will underline inconsistently named variables etc. Big companies have some variable naming (and code formatting) standards, you can find them easily, search "Mozilla C++ coding style", "Google C++ Style Guide" etc.
try implementing vector using the the specifications on cppreference, just by encountering custom allocators in a practical setting actually teaches you a whole lot about them (tip : look up std::allocator_traits first to see how c++17 and on handles allocators. )
I think there is a bug in reallocate function. If we enter in that if statement : if (new_capacity < m_size) m_size = new_capacity; then I think in for loop when we call destructor of each element, then if old m_size is bigger then new m_size then I think we do not call destructors of all elements. Correct me if I'm wrong. Great job btw :)
Possible fix: At the start, save the current size. After the loop, enter in a new loop starting from the current size till the previous size. Then call destructors for those.
Check out the time 22:32, Only four "copy" get printed to the console, which made me confused, I think there should be five "copy", three of them from the right value when creating those three Vector3 objs, the other two “copy” are from ReAlloc when the vector's size increase from 2 to 3, then I realized in Cherno's video, the information printed may not be complete, so indeed five copies should be printed, am I right?
This is a bug: newBlock[i] = std::move(m_Data[i]); "newBlock" is uninitialized memory - calling move assignment method will likely lead to a crash (depending on T). Do this instead, using the move-constructor: new (newBlock + i) T(std::move(m_Data[i]));
Yes I don't know about you but I get a runtime error with the original code and I ended up with the same conclusion. For some reason, The Cherno does not seem to have this issue...
@@arnaudgranier3045 Either the compiler is zeroing the memory for him (which would just happen to work for a lot of objects, if all zeros are a valid state), or he got lucky. A proper debug build would initialize that memory to 0xfefe to shine a light on this bug.
Thanks for the great video/exercise! - Although I have noticed something that I am quite confused about. In your Vector class, you create a pointer to the heap-allocated array containing the objects in the vector, then use that pointer whenever you are directly accessing/changing the objects/size of the array. Wouldn't it be easier to create the pointer, then have a reference to the pointer, and use the reference when you can and use the pointer when necessary (e.g. deleting memory, reallocating memory). This should work since the reference will always be pointing to the pointer (dereferenced), and when reallocating memory, the pointer's contents will be different, but the place of memory which holds that pointer will be the same, so the reference to the pointer is not changing. The reason I'm thinking this is because, in your references video, you said to always use a reference if you can, and only use pointers when necessary. If this is a bit confusing, here is what I mean: . // Example class Vector public: // All the public methods and things private: T *m_data_ptr = nullptr; // Create pointer to array containing vector objects/elements (currently set to nullptr) T &m_data_ref = *m_data_ptr; // Reference to dereferenced pointer to array containing vector objects/elements // Use m_data_ref when possible . Is this a good thing to do?
It would not work, because the one thing you cannot do with references is changing what they are referring to. That means, if you have to reallocate the buffer, your reference will always reference the old buffer you already have deleted.
If anyone else is confused to why std::string isn't working, my understanding is that in the PushBack function or wherever you're setting values in m_Data you actually need to placement new the object into place not use the assignment operator here. Basically you'll wanna replace m_Data[INDEX] = newValue; TO new(&m_Data[INDEX]) newValue I'm fresh to this idea so if anyone has a better description of why, it'd be much appreciated. Thanks!
How should I return a range of the vector? Vector getRange(size_t startIndex, size_t count) const { Vector tempVector= Vector(); for (size_t i = startIndex; i < startIndex + count; i++) tempVector.pushBack(std::move(this->itemsArray[i])); return tempVector; // This is wehre it calls "tempVector" destructor } Because it deletes tempVector via destructor and then returns empty vector.
Wee it is warm here in Sweden now. I agree data structures are important. I realized type casting can be 'very bad' in Flutter. I call MySql get back array of Json objects, used an online example to get the socket io. Problem was just that casting to MAP is wrong since it only works in one dimension. So put var(VAR) let the structure be as it is. Never typecast if not absolutely necessary. Still a bit confounded with the place in development but it is all clear really. Next step is... the rest i just make it bit by bit as we say. Great video by the way!
Hi @TheCherno, there is a slight issue in your Clear() function. You probably know about it, but i will mention is anyways because there is no mention in the video and it took me a couple of hours to solve it. (Yes i have watched Iterators video, but this issue was not solved (maybe im wrong) there). Clear function does not release memory allocated for m_Data, it only calls for destrustors. In order for Clear function to work properly, it need to look like this: void Clear() { for (size_t i = 0; i < m_Size; i++) { m_Data[i].~T(); //calling all the destructors. } ::operator delete(m_Data, m_Capacity * sizeof(T)); // releasing memory, also, wouldnt this call also call each destrustor? Im not sure, maybe you can check it. m_Data = nullptr; // this one is important, because if we call Clear second time right after first call, without allocating new block of memory, there will be a memory access violation m_Size = 0; m_Capacity = 0; //can be 2 if ReAlloc does not fix bug, where if capacity is 0 vector can not grow. }
Clear is not supposed to release the memory, it is just a function that's supposed to remove all elements from the vector. The m_Data buffer should be left like how it is because we may and will probably still work on the vector after calling Clear on it. We only release the memory whenever we want to destruct the vector object. This idea is the exact same with std::vector or many other vectors. EDIT: Also placement delete doesn't call destructors.
หลายเดือนก่อน
I implemented matrices with std::valarray and it is beautiful, makes multiplication much easier.
But what if you call Clear() on the object, and then when the object goes out of scope, its destructor calls Clear() too, leading to the same error again?
I implemented mine such that the container is also a shared pointer, and used the placement new approach in the first pass. Also the memory is coming from a "memory manager" that allocates it from some internal heaps and you can specify which heaps you want to use at instantiation. I do wonder if the Realloc step could just use memcpy instead of calling placement new and moving each element from the "old" container buffer to the "new" one.
yeah, since he's just moving the bytes to a new array memcpy would have been perfect. He doesn't want to use the copy constructor because that would create deep copies which would be a bug, you want the same old objects moved to a different place in memory.
cant get the variadic emplaceBack function to work at all, with or without std::forward. its exactly how you have it in the video, too. might be because im using Clang instead of MSVC. it throws an error about "Excess elements", which i guess means its not going through more than one.
I hope very much that the std::vector is NOT written like that (17:20) because downsizing without deleting (calling destructors) of excessive vector elements is a direct path to a memory leak.
Great video! It looks to me that there is a potential memory leak in ReAlloc function if new capacity is smaller than size, Desctructors will be called only on first newCapacity elements, or am I missing something?
High quality video as always! Those object lifetime issues are really nasty, which always make me appreciate the work of our STL implementors more. Apart from a proper Rule of Five in the vector class, other features like standard allocator & iterator support and stronger exception guarantees, though tricky to implement, are also needed for a complete vector class. I would really appreciate it if you could cover these topics. Again thanks for your good work!
It is a very good idea at the beginning of the video to encourage viewers to go on and implement their own vector class. However, to help get people started you may perhaps give a little push by also giving what it should be capable of (methods? interface? header file? a code segment that uses a vector and can successfully compile and run if someone has implemented a vector class correctly). Otherwise, a very adventurous viewer may start implementing something huge and never returning to your video. Or a passive kind just shrug their shoulders and go on watching without spending a second on thinking how it could be done. Or, alternatively or supplementing the previous idea, you can also set a time limit: "let's see how many of the following methods/capabilities you can implement in 30 mins or 60 mins. (a time-limit in most cases can help focus.)
very interesting video and series really! I have one comment though. In your Vector3 class implementation of the move assignment operator (line 41) I think you should call delete m_MemoryBlock before assigning other.m_MemoryBlock to it, otherwise a memory leak is created. Correct? But then, if you do so, each time you move a Vector3 element to the vector (PushBack of ReAlloc function) you get a memory access violation because you are moving the element to an uninitialized memory block and the delete m_MemoryBlock in Vector3 move assigment fails. To solve that I think it is necessary to initialize to 0 the memory allocated with ::operator new every time this is used, with something like std::memset(m_data, 0, m_capacity * sizeof(T)); In this way the delete is called on a nullptr and does not fail. Am I wrong? Thanks.
Yes, you're a lifesaver! I had this exact issue while trying this Vector class out with my own implementation of String, it would indeed crash when calling the move assignment operator was called on non-initialised memory. However I still found a case for a crash: when calling pop_back, my own String deleted its m_Data block but did not set it to nullptr; So when calling push_back again afterwards, the same memory access violation happens. The fix was to memset to zero also on pop_back() and on clear(). Another fix would be to edit my String destructor to set the m_Data to nullptr, but I find it a cleaner solution to do this within Vector. EDIT: he addresses this at the end of the "Writing an iterator in C++" video. The clean solution to fix this issue is with "placement new" construction (like used in emplace_back) instead of the assignment operator.
0:25 Okay I can't take 4 dimensions anymore. I need to see how it looks. So I'm going to challenge you to create a 4D renderer. (I think it's suitable because you like graphics programming the most, correct me if I'm wrong.)
I did my own implementation using std::move(), it works fine on reallocating, but only in the release mode, and crashes with "heap corrupted" error in the debug mode.
hmm, why would it? std::string is responsible for it's own allocations and deallocations, and as long as the vector doesn't call the destructor twice, then it wouldn't.
So issue with the bug is that popBack is calling the destructor and freeing the internal heap allocated memory on the last element but it is not deallocating the actual space in the vector for that element(which is heap allocated itself) So when the Vector object's scope ends its destructor calls delete [] m_Data on all elements which goes through every element to destruct them and free the space allocation. But it will not take into account that m_Size has been decremented by popBack so it tries to call the destructor on all elements including the last element that was destructed by popBack leading to double deletion. Hopefully I got that right.
Hey Cherno! Thanks for the video, your content progresses every video and it looks amazing. It would be amazing if you made a video about custom memory allocators. I'm currently making my own engine and it appeared to be a very important topic. Although I've read several books on the topic, I never seen an example of an implementation with usage examples. That'd be a great topic!
Getting an segmentation violation signal in the push_back function and have no idea why. All i'm getting is that is is caused by a READ memory access, which is m_Data[m_size] = value;
The vector3's constructor and move assign function have problems, "m_MemoryBlock"cannot simply assign value(m_MemoryBlock = other.m_MemoryBlock;), if we push back a vector3 value(Vector3 v3(1, 2, 3); vec.push_back(v3); ); it will double free(v3 free once, vec free second)
is this code available to check somewhere? I haven't quite understood the last part with delete with non trivial data types and variadic templates thing.
It is cool content! Thank you very much, Cherno!!! Like pressed! One thing I wish the vid had is a link to a git repo with code. Though, I am coding the data structure along with you, some place to look up code would not hinder (for educational purposes) 🤠
I'm getting a problem with PopBack. My editor/compiler isn't seeing 'T' as something valid within the scope of the function, and as such when I try to call the destructor on T, the next time that it tries to print the value from my Vector it's printing garbage (negative infinity). not sure what's wrong or how to fix this, I went back to the part in the video but couldn't figure out what I could have done wrong, and I'm not sure what to search for to figure out how to fix it my code, for reference: void PopBack() { if (m_Size > 0) { m_Size--; m_Data[m_Size].~T(); } }
Thanks for your video, I really enjoy all your series. I have a question about T* newBlock = new T[newCapacity]. when it declared, why it shouldn't be deleted like the "delete[] newBlock" after "m_Capacity=newCapacity"?? Is that not leaking a memory ? if you do pushbacks 100 times, newBlock would stay 100 memory blocks in memory , am I incorrect? Please teach me.
m_Data gets set to newBlock, so if you delete newBlock, you essentially delete m_Data, which is a big no no. You only need to delete the old data (m_Data) before you set m_Data to newBlock. After that both newBlock and m_Data point to the same memory address.
This is very strange timing because I just coded my own version of a vector class a few days ago. I'm curious how much different my implementation was from yours
No. as you're copy/moving the elements to a new block of data, and then deleting the original data.
2 ปีที่แล้ว +1
1. std::construct_at instead of operator=(T&&) in emplace (Edit: after some research I discovered std::construct_at is introduced in c++20, for c++17 and below, new() must be used) 2. you violated rule of 0/3/5 implement those ctors/ops! (TIL I learned there was a malloc/free counterpart in c++ in the form of ::operetor new/delete)
Anyone know why I am getting Copy Destroy twice before the 'Copy Copy Destroy Destroy Copy Destroy'? In debugging each time it pushes back a new object, Copy and Destroy get printed, so why is there only one showing in the video even though 2 objects are pushed back? Thanks.
new(&m_Data[m_Size]) T(std::forward(args)...); how cools is that 😂 that's awesome 😎 I didn't know that I can invoke new operator like that, thx ✌ By the way, examples like this are the best how to learn and understand how underlying std library works. 👍
That's called "placement new", and it is indeed really powerful! Have a look at memory pooling and custom allocation, the whole subject is quite interesting.
Capacity is how many elements can fit in allocated memory, while size is how many actual initialized elements there are. That means that there’s uninitialized memory (or remains of already destructed elements, e.g. if you called pop_back) outside of size range, and you really don’t wanna call dtor there
@@DeusAmentiam But, newCapacity can be greater than m_Capacity, m_Size in this situation can be greater than m_Capacity and for loop has been raise an error.
@Peter Konský Oh, I made a mistake. Thanks. But, i see another situation. newCapacity less that current m_Size and we are assign m_Size = newCapacity in this situation. Than we are call destructor not for all actual elements in our array. This is an error, does it ?
What are the benefits of overloading operators for a vector class? Wouldn't that make many things super confusing? Is V1 * V2 the Dot or the Cross-Product? What do you guys think about OO?
Good point about using directly raw pointers in this type of cases (around the start of minute 15)! It got me thinking, just for the fun of understanding, how would a smart pointer (std's unique_ptr or shared_ptr) substitute the T* newBlock = new T[newCapacity]?
You can do it like this: std:: unique_ptr newBlock = std::make_unique(newCapacity); The advantage of this is it will call the correct version of the delete operator for you! However, I would still use raw pointers if I'm writing a custom container.
If the move assignment (which should really be a placement new to call the constructor) throws an exception, newBlock will not be deleted and you leak memory. A unique_ptr would prevent that. However, in that particular case, there should be a try-catch-block to ensure destructors are called on exactly those elements that have been succesfully move constructed. The more you think about it, the harder it gets to make this function bug free. In many other cases unique_ptr would be a great help, but not here.
Since you no longer need to call constructors and destructors in the ReAlloc function, could you use memcpy instead of looping and copying each element one by one?
No, at least not always, suppose in your constructor you store the this pointer in some member variable. The memcpy would copy over the old/invalid pointer. I just ran into this.
I really enjoy exercises like these (std::tuple having been my favorite so far) and what i do is to go to cppreference and copy over the function signatures to be implemented later. this forces you to encounter things like custom allocators (which greatly improved my ability to optimize my code in larger projects) and learn how versatile templates can be. In the case of vector i actually, rather than keeping track of sizes just kept track of a : struct { struct { pointer begin, end; } allocation, elements; } m_data; which has some time saving properties to it.
When calling "T* new_block = ::operator new(new_capacity * sizeof(T));", I get a "new_block" that is only 8 bytes in length, i.e. "sizeof(new_block) == 8". Since our "Vector3" type is 32 bytes in length, I even tried being explicit about it and doing "T* new_block = ::operator new(64);" (allocating space for 2 Vector3 elements on first reallocation) just to help me debug. But even for that call, I still get a "new_block" that is 8 bytes long. Am I doing something wrong when calling ::operator new()? Anyone have any idea why I'm getting the same size allocation regardless of what I pass to ::operator new()? Should we be using ::operator new[]() instead?
Hope you all enjoyed the video! Have a go at the exercises and implementing some of the features we didn't get to in this video (eg. copy/move constructor + operators for the Vector class). And finally don't forget that the first 1000 people who click the link will get 2 free months of Skillshare Premium: skl.sh/thecherno0820
You could branch on a if constexpr(std::is_pod::value) and if it is, just memcpy things, otherwise copy construct.
I am really happy with this video! While you provided the solution, I wish you would have also highlighted the issue of the old allocation: it constructs! Imagine if your class had a Sleep in the constructor, or more realistically, a heap allocation. With out operator new, it would slow you down substantially whenever you wanted to resize your vector. Especially over thousands of items.
Hello! Thank you for the video and for your work!
I found that the... outcome of this code depends strongly on the compiler. May I humbly suggest a topic for a video about compilers in more details, which one do you use with what flags and what are the pitfalls or gcc, g++, clang, calng++.
I am working in Ubuntu and I found that the only compiler that works for this project is "rustc" that I've never heard of before :)
Bro please don't give up on finishing this series ...u r all I can rely on.🙏
amen
This is literally my first stop before reading text book.
Recreating the vector class is a good exercise to explore and experiment various useful features of C++
Love this topic: Data Structures in C++ . Thank you soooo much .
Learning data structures in c++ makes it much easier to understand and do in other languages. C++ was the language used in my comp sci program for intro to computer sci and for data structures and algorithms and now I’m doing data structures and algorithms in JavaScript preparing for interviews and that foundation in c++ helps a ton.
If anyone is a student and has to do c++ in school, don’t be overwhelmed. Trust the process, work hard, and you’ll do just find.
Cherno clutch yet again with perfect quality content!
About 2 years ago I really became intrigued by stl. The vector class was the first one I tried recreating. And failed miserable. How ever I have redone this exercise about 20 times since. And I can write a vector class with the majority of the features one would want in about 1 day. I encourage everyone to do this exercise every once in a while. You always learn somethimg new.
I am planning on writing my own stl
You still have a bug in ReAlloc function in line 95: newData[i] = std::move(...); - you can't use assignment operator here (either move or copy) because newData[i] is uninitialized memory - it is not the object of type T and by calling operator= (again move or not) you treat it as if it is object of type T. So instead of that you should use placement new once again so change that line to: new (&newData[i]) T(std::move(m_Data[i]));
Thank you man!!!! I am looking for exactly the fix for this issue! The code does not work for std::string, it crashes exactly at this line!! Thank you man I appreciate it!
Yes, I've noticed that too. I'm mostly going by myself but my ReAllocate function is very similar to this. My constructor for a class allocates some memory but the constructor is not called so when assigning (copy is called) the data is just leftover memory.
he has addressed this issue in the writing an iterator video
hey newBlock is of type T* so I replaced the code you gave, but now its showing Buffer overrun while writing to 'newBlock' error 'Vector3::Vector3(const Vector3 &)': attempting to reference a deleted function
It is initialized. When you create your array as "new T[size]" it initializes every element with default constructor. Which is a problem, because what if your value_type doesn't have default constructor? To create uninitialized array you need to use allocator. I also would suggest to use std::construct_at instead of placement new operator. It does the same thing, but more descriptive and easier for an eye.
Would love to see a video on variadic templates someday :)
With forwarding references...
Implementing an std::invoke clone
I'm guessing you would live to regret it
Urgh, that...
We’re still waiting for his Exceptions vid he promised around 4 years ago 😂
I'm really enjoying these longer C++ videos, keep it up ! and thanks !!
40:13 Constructors are being called there if you do a cout in your constructors you will see them print. But I assume the point being that they don't need to be called so thats why we switch to an operator new.
Wow, that gotcha moment at the end was so inconspicuous that I'm a bit shaky in writing efficient C++ code and debugging it. May this series never end for the good of me!
For all those who did make it to 36:05, and then got lost after "HAHA did you think that was it?", Kudos to you.
Somebody explain to me
@@sandeepkalluri Essentially, delete operator does 2 things: call dtor, and then free memory. delete[] call dtor of each of its elements, then free memory. The problem is, if we manually call dtor of some item, it will in fact delete its members memory(int* in our Vector3 example), but the Vector3 instance is still physically present in the contiguous array of our Vector instance. That means that if we call the Vector delete[] method, ie when the Vector instance is out of scope in this case, Destructor of each item may be called Twice ! The problematic items here are the one cleared or popped back.
When delete[] iterates through items to call dtor and free, it will not stop at our m_Size defined value, but out Clear implem does.
If you want to check stuff by yourself, just simply emplace and immediatly pop some object from Vector in debug mode. You can clearly see that in the memory our object is still living after pop. Destructor of an instance manages its member destruction, not the object itself.
We have a similar concern with the new operator(although it doesn't cause crashes): it allocate memory, then call ctor. In case of re allocating resource (or "reserving"), we want to make sure we have enough space for our future item to be added. We do not want to actually create default objects waiting to be overwritten by future actual pushed back ones.
That is why we use this syntax:
::operator new
which only allocate UNINITIALIZED memory, ie memory we own but may contain unpredictable garbage.
Hope this helps.
@@sandeepkalluri
new calls the allocator to allocate memory, with size of the type you passed in, and then call the constructor to create an object on that memory allocated
delete calls the destructor first, and then calls the deallocator to deallocate memory
the ::operator new only allocates memory, and it does not construct anything, the memory is now yours, but it has nothing in it,
to construct an object of type T in that memory, you have to do something called a placement new
new(address) ctor(data)
so the normal new operator that you would normally use, it does two things automatically, allocate, then construct
in this video, what you're doing is you do those two things by your own
the same goes with ::operator delete(ptr, size * sizeof(T))
it only deallocates, and does NOT call the destructor
so what the normal delete would do is to call destructor, then call the ::operator delete behind the scenes, the deallocate
since in this video, you have a lot of parts that you call the destructor explicitly, and then finally you call the normal delete, which calls the destructor again, despite that it has already been called at some other part of the code
if you're sure about calling the destructor exactly where and when you wanted, then all you need is the deallocator
And since you can't call ::operator delete on the normal new, you have to also do the two task: allocate by using ::operator new and placement new manually, just so the ::operator delete can work, deallocates what ::operator new has allocated
The inplace new and operator new/delete part was new to me, and really amazing. I knew I'll learn something new from your video.
After all the fame, the fact that you keep this C++ series for dummies like me going, is amazing! The game engine series is way over my head and the reaction videos are not really my thing.. but I do hope that you are getting all the compen$ation you want because you deserve it... kuddos mate
It will be great if you consider to modify your custom vector class to work with range-based "for" loop and show us how
i think that's what he meant when he said he'll make a video on iterators
Already figured out how. I have just write begin() and end() methods which returns something acts like iterator, but i done this in custom string class, in vector class it should work the same way.
42:48 Is this assignment valid in 96 line (underlined by the way) ?
newBlock[i] = std::move(m_Data[i])
It is assignment xvalue object to raw memory. Is it ok ?
Shouldn't be placement new again something like that ?
new(&newBlock[i]) T(std::move(m_Data[i]))
If yes, then maybe your code has UB but did not crash.
It should be a usual move assignment operator...
@@donrumata5299 Why ? newBlock is raw memory (like malloc allocated)
I think it doesn't matter how the memory where the object gets copied looks like. It's just important that there is memory that can be written. The move constructors initialises the memory in the raw area how it needs. Every time a c++ class gets allocated the constructor writes data into raw memory.
The member functions and stuff gets not stored in the allocated memory, if that was your doubt. The allocated memory just holds data and on construction the data is always raw.
Hope this helps a bit.
@@rafalmichalski4893 the idea is that there cannot be a "raw" memory. If you have permission to manipulate it, it doesn't differ from any other memory
you are correct. the implementation in the video can mess up if you create a vector of a class with virtual methods, since the vtable pointer of the object won't be initialized.
Please don't give up on this series. I started it yesterday and I am on video number 20. I love the way you teach.
This guy is so good
Since We're big boys here... 8:25
It cheered up my dull day, thanx cherno
I don't know if you read all your comments but I just wanted to say I really am liking you breaking things down!
I have added more constructors which takes arguments as initializer_list :
1. Vector(const std::initializer_list&);
2. Vector(std::initializer_list&&);
Then I have created a Student class and created three objects of it s1,s2,s3
And then I try to do following:
Vectorstudents = { s1, s2, s3};
Vectornumbers = { 1,2,3,4,5,6,7,8,9 };
So as I am passing s1,s2,s3 as "lvalues" for students and 1,2,3,... as "rvalues" for numbers.
But in both of cases it is calling 2nd constructor. So why Vectorstudents also calling 2nd constructor? Can you help me ?
I think it's because in both cases you're still initializing the vector with temporary instance of initializer_list (so r-value). If you want to call the l-value constructor, you'd have to do something like:
std::initializer_list list = { s1,s2,s3 };
Vector students = list;
Very cool! I think I “know it all already” but usually learn something new in these! I’m writing my own containers now for my personal engine project and great to get some ideas from The Cherno 😀
Also might need to check for “self” in your “moves”
I like the honesty about difficulty and serious attitude towards learning c++
This video inspired me to create a sort of a mish-mash of two data types together, basically a compromise b/w a linked-list and a vector.
What if you have moderate-sized arrays that link to each other?
Array[10]->Array[10]->Array[10]->... and so on?
i) This way, you don't have to make as many hops for reaching an element. MishMash[35] is actually just two hops away, rather than 35 hops!
ii) You still have to allocate a new buffer for every 10 inserts (this could also grow exponentially), but you don't need to copy the old buffer.
I feel like this data type can be used for vectors that dramatically alter in size frequently.
Thank you so much Cherno, this video is absolutely phenomenal
an optimization that i use in my game engine array class, is using type traits to check if T is a POD, and if so using memcpy (not memmove for performance) to copy elements instead of using std move. compilers may notice this, but i added it just in case
42:06 line 98-99 calling the destructor on m_Size works if newCapacity >= m_Size. But if newCapacity < m_Size, you might miss out destructing some objects in m_Data?
I was halfway happy that I had done pretty much the same and then I realized he was going to implement a lot of the vector interface :D
Stack allocated vector that is actually just a single node in a linked list. Every so often you get a slower step, but I am so intrigued to try implementing it
I tried to figure out an function to do the doubling of the capacity initially, but heavily dampening as the capacity grows, but nothing was really that satisfying. Like it'd always still grow quite a lot once it reached like 500. Which is obviously quite unnecessary as you'll take forever to fill it even if you had that much more data. Or it wouldn't grow a lot in the beginning which is also the opposite of what you'd want to. Without doing if checks for the size, that'd also be the opposite of what we want, building processing cost trying to save it.
Can you make a video explaining your coding style and naming conventions?
I think it would be cool to see a little video about why you prefer PascalCase and prefixes on variables (like m_ and s_) and what are your tips for choosing naming and coding conventions.
It would also be cool if you could talk about your C# conventions as well (as far as I know, the m_ prefix isn't very popular in C#, so I think it would be cool to know if you stick to it and why).
He once said that he used to use camelCase back when we was doing java but that after sometime at EA he got so much used to PascaCase that he cant use camelCase anymore (sorry bout my english)
Choose what do you personally prefer (or make a consensus within the team you are working with). Some IDEs (e.g. CLion or VS w/ReSharper) allow to set preferred naming conventions in Settings and they will underline inconsistently named variables etc. Big companies have some variable naming (and code formatting) standards, you can find them easily, search "Mozilla C++ coding style", "Google C++ Style Guide" etc.
Thank you for the great video! Will you do a tutorial on custom allocators some day? There are not really that many tutorials on it.
try implementing vector using the the specifications on cppreference, just by encountering custom allocators in a practical setting actually teaches you a whole lot about them (tip : look up std::allocator_traits first to see how c++17 and on handles allocators. )
I think there is a bug in reallocate function. If we enter in that if statement :
if (new_capacity < m_size)
m_size = new_capacity;
then I think in for loop when we call destructor of each element, then if old m_size is bigger then new m_size then I think we do not call destructors of all elements.
Correct me if I'm wrong.
Great job btw :)
Yeah, that's true. This is very scary because it can lead to memory leaks if the T object is supposed to free some memory in the destructor.
Possible fix: At the start, save the current size. After the loop, enter in a new loop starting from the current size till the previous size. Then call destructors for those.
Check out the time 22:32, Only four "copy" get printed to the console, which made me confused, I think there should be five "copy", three of them from the right value when creating those three Vector3 objs, the other two “copy” are from ReAlloc when the vector's size increase from 2 to 3, then I realized in Cherno's video, the information printed may not be complete, so indeed five copies should be printed, am I right?
Still not sure why only four "Copy" get printed, not five
This is a bug:
newBlock[i] = std::move(m_Data[i]);
"newBlock" is uninitialized memory - calling move assignment method will likely lead to a crash (depending on T).
Do this instead, using the move-constructor:
new (newBlock + i) T(std::move(m_Data[i]));
Yes I don't know about you but I get a runtime error with the original code and I ended up with the same conclusion.
For some reason, The Cherno does not seem to have this issue...
@@arnaudgranier3045 Either the compiler is zeroing the memory for him (which would just happen to work for a lot of objects, if all zeros are a valid state), or he got lucky. A proper debug build would initialize that memory to 0xfefe to shine a light on this bug.
Well done bro, full support from India.
And thank you for amazing advanced tip.
Thanks for the great video/exercise! - Although I have noticed something that I am quite confused about. In your Vector class, you create a pointer to the heap-allocated array containing the objects in the vector, then use that pointer whenever you are directly accessing/changing the objects/size of the array. Wouldn't it be easier to create the pointer, then have a reference to the pointer, and use the reference when you can and use the pointer when necessary (e.g. deleting memory, reallocating memory). This should work since the reference will always be pointing to the pointer (dereferenced), and when reallocating memory, the pointer's contents will be different, but the place of memory which holds that pointer will be the same, so the reference to the pointer is not changing. The reason I'm thinking this is because, in your references video, you said to always use a reference if you can, and only use pointers when necessary. If this is a bit confusing, here is what I mean: . // Example class Vector public: // All the public methods and things private: T *m_data_ptr = nullptr; // Create pointer to array containing vector objects/elements (currently set to nullptr) T &m_data_ref = *m_data_ptr; // Reference to dereferenced pointer to array containing vector objects/elements // Use m_data_ref when possible . Is this a good thing to do?
It would not work, because the one thing you cannot do with references is changing what they are referring to. That means, if you have to reallocate the buffer, your reference will always reference the old buffer you already have deleted.
Funny you put this video up, Just today and yesterday i've been working on my own spin on this.
Thanks, mate! I really learned a lot from making my own version of the STL vector.
At 19:40 what did he mean like what should we do can anyone tell me pls I'm not experienced in advanced stuff
If anyone else is confused to why std::string isn't working, my understanding is that in the PushBack function or wherever you're setting values in m_Data you actually need to placement new the object into place not use the assignment operator here. Basically you'll wanna replace
m_Data[INDEX] = newValue;
TO
new(&m_Data[INDEX]) newValue
I'm fresh to this idea so if anyone has a better description of why, it'd be much appreciated. Thanks!
Try this
void PushBack(T&& value)
{
if (m_Size >= m_Capacity)
Realloc(m_Capacity + m_Capacity / 2);
new(&m_Data[m_Size]) T(std::move(value));
m_Size++;
}
for anyone getting a bug for making a string vector, he has addressed the issue in the writing an iterator video.
How should I return a range of the vector?
Vector getRange(size_t startIndex, size_t count) const
{
Vector tempVector= Vector();
for (size_t i = startIndex; i < startIndex + count; i++)
tempVector.pushBack(std::move(this->itemsArray[i]));
return tempVector; // This is wehre it calls "tempVector" destructor
}
Because it deletes tempVector via destructor and then returns empty vector.
Wee it is warm here in Sweden now. I agree data structures are important. I realized type casting can be 'very bad' in Flutter. I call MySql get back array of Json objects, used an online example to get the socket io. Problem was just that casting to MAP is wrong since it only works in one dimension. So put var(VAR) let the structure be as it is. Never typecast if not absolutely necessary. Still a bit confounded with the place in development but it is all clear really. Next step is... the rest i just make it bit by bit as we say. Great video by the way!
34:00 If we use "new", don't we need to have a "delete" somewhere too? To avoid memory leak?
Hi @TheCherno, there is a slight issue in your Clear() function. You probably know about it, but i will mention is anyways because there is no mention in the video and it took me a couple of hours to solve it. (Yes i have watched Iterators video, but this issue was not solved (maybe im wrong) there).
Clear function does not release memory allocated for m_Data, it only calls for destrustors. In order for Clear function to work properly, it need to look like this:
void Clear() {
for (size_t i = 0; i < m_Size; i++) {
m_Data[i].~T(); //calling all the destructors.
}
::operator delete(m_Data, m_Capacity * sizeof(T)); // releasing memory, also, wouldnt this call also call each destrustor? Im not sure, maybe you can check it.
m_Data = nullptr; // this one is important, because if we call Clear second time right after first call, without allocating new block of memory, there will be a memory access violation
m_Size = 0;
m_Capacity = 0; //can be 2 if ReAlloc does not fix bug, where if capacity is 0 vector can not grow.
}
Clear is not supposed to release the memory, it is just a function that's supposed to remove all elements from the vector. The m_Data buffer should be left like how it is because we may and will probably still work on the vector after calling Clear on it. We only release the memory whenever we want to destruct the vector object. This idea is the exact same with std::vector or many other vectors.
EDIT: Also placement delete doesn't call destructors.
I implemented matrices with std::valarray and it is beautiful, makes multiplication much easier.
Thx for the video. How did you do, what you did at 22:14? (initializer list without typing)
But what if you call Clear() on the object, and then when the object goes out of scope, its destructor calls Clear() too, leading to the same error again?
just can't help waiting for rbtree-based set explained by The Cherno
rbtree-based set gives me PTSD.
@@dominicjung4950 absolutely. deletion is kinda magic, and most of tutorials skip this thing
@@aquavitale3551 It requires very weird recolouring but you can go on geeksforgeeks it explains it quite well.
Great video! Do you plan to do some algorithms like breadth first search in the future?
I implemented mine such that the container is also a shared pointer, and used the placement new approach in the first pass. Also the memory is coming from a "memory manager" that allocates it from some internal heaps and you can specify which heaps you want to use at instantiation. I do wonder if the Realloc step could just use memcpy instead of calling placement new and moving each element from the "old" container buffer to the "new" one.
yeah, since he's just moving the bytes to a new array memcpy would have been perfect. He doesn't want to use the copy constructor because that would create deep copies which would be a bug, you want the same old objects moved to a different place in memory.
cant get the variadic emplaceBack function to work at all, with or without std::forward. its exactly how you have it in the video, too.
might be because im using Clang instead of MSVC.
it throws an error about "Excess elements", which i guess means its not going through more than one.
Would setting all pointers to heap memory in the destructors to nullptr also solve the problem at the end of the video?
I hope very much that the std::vector is NOT written like that (17:20) because downsizing without deleting (calling destructors) of excessive vector elements is a direct path to a memory leak.
This was harder than I expected.
I didn't understand one thing... How should you implement the destruction of allocated structs or pointers?
Great video! It looks to me that there is a potential memory leak in ReAlloc function if new capacity is smaller than size, Desctructors will be called only on first newCapacity elements, or am I missing something?
I really enjoy watching your videos! Keep up the good work!
Спасибо тебе! Для начинающего трудно, но очень интересно! Хотя предыдущее видео понятнее про массивы.... Спасибо!
High quality video as always! Those object lifetime issues are really nasty, which always make me appreciate the work of our STL implementors more. Apart from a proper Rule of Five in the vector class, other features like standard allocator & iterator support and stronger exception guarantees, though tricky to implement, are also needed for a complete vector class. I would really appreciate it if you could cover these topics. Again thanks for your good work!
Here's a tip: if you can code slowly, you can code quickly.
Suvi-Tuuli Allan But sacrilegious code is often not permitted XD
OMGGG NEW CHERNO VIDEO AND IT'S 45 MINUTES LONG IM SO FREAKING EXCITED OMG
It is a very good idea at the beginning of the video to encourage viewers to go on and implement their own vector class. However, to help get people started you may perhaps give a little push by also giving what it should be capable of (methods? interface? header file? a code segment that uses a vector and can successfully compile and run if someone has implemented a vector class correctly). Otherwise, a very adventurous viewer may start implementing something huge and never returning to your video. Or a passive kind just shrug their shoulders and go on watching without spending a second on thinking how it could be done. Or, alternatively or supplementing the previous idea, you can also set a time limit: "let's see how many of the following methods/capabilities you can implement in 30 mins or 60 mins. (a time-limit in most cases can help focus.)
very interesting video and series really! I have one comment though. In your Vector3 class implementation
of the move assignment operator (line 41) I think you should call delete m_MemoryBlock before assigning other.m_MemoryBlock to it, otherwise a memory leak is created. Correct? But then, if you do so, each time you move a Vector3 element to the vector (PushBack of ReAlloc function)
you get a memory access violation because you are moving the element to an uninitialized memory block and the delete m_MemoryBlock in Vector3 move assigment fails.
To solve that I think it is necessary to initialize to 0 the memory allocated with ::operator new every time this is used, with something like std::memset(m_data, 0, m_capacity * sizeof(T));
In this way the delete is called on a nullptr and does not fail. Am I wrong? Thanks.
Yes, you're a lifesaver! I had this exact issue while trying this Vector class out with my own implementation of String, it would indeed crash when calling the move assignment operator was called on non-initialised memory.
However I still found a case for a crash: when calling pop_back, my own String deleted its m_Data block but did not set it to nullptr; So when calling push_back again afterwards, the same memory access violation happens. The fix was to memset to zero also on pop_back() and on clear(). Another fix would be to edit my String destructor to set the m_Data to nullptr, but I find it a cleaner solution to do this within Vector.
EDIT: he addresses this at the end of the "Writing an iterator in C++" video. The clean solution to fix this issue is with "placement new" construction (like used in emplace_back) instead of the assignment operator.
LOVE THIS SERIES. THANK YOU.
0:25 Okay I can't take 4 dimensions anymore. I need to see how it looks. So I'm going to challenge you to create a 4D renderer. (I think it's suitable because you like graphics programming the most, correct me if I'm wrong.)
Not all the dimensions need to refer to physical dimensions. The fourth dimension is usually time ;)
I did my own implementation using std::move(), it works fine on reallocating, but only in the release mode, and crashes with "heap corrupted" error in the debug mode.
Can I write a templated dynamic array that can be saved to and loaded from disk?
"Hopefully bug-free" yup, try using it with std::string
Yeah. How to resolve if the template argument is std::string, the program still crash
hmm, why would it? std::string is responsible for it's own allocations and deallocations, and as long as the vector doesn't call the destructor twice, then it wouldn't.
off topic, but does anyone know what theme Cherno uses? I find the oranges and other colours quite appealing but haven't been able to find a match.
So issue with the bug is that popBack is calling the destructor and freeing the internal heap allocated memory on the last element but it is not deallocating the actual space in the vector for that element(which is heap allocated itself) So when the Vector object's scope ends its destructor calls delete [] m_Data on all elements which goes through every element to destruct them and free the space allocation. But it will not take into account that m_Size has been decremented by popBack so it tries to call the destructor on all elements including the last element that was destructed by popBack leading to double deletion. Hopefully I got that right.
Amazing course! Thank you a lot for this "demo"! 🙏
if does not work for T is type of "std::string". how can i make it ?
Doesn't assignment operator will be called while push back instead of copy ?
Thanks man i was literally finding how to make ouch back function interally and also how to increase capacity if capacity is ful
Would love to see a video on creating class String from scratch!
Hey Cherno! Thanks for the video, your content progresses every video and it looks amazing.
It would be amazing if you made a video about custom memory allocators. I'm currently making my own engine and it appeared to be a very important topic. Although I've read several books on the topic, I never seen an example of an implementation with usage examples. That'd be a great topic!
Getting an segmentation violation signal in the push_back function and have no idea why.
All i'm getting is that is is caused by a READ memory access, which is m_Data[m_size] = value;
The vector3's constructor and move assign function have problems, "m_MemoryBlock"cannot simply assign value(m_MemoryBlock = other.m_MemoryBlock;), if we push back a vector3 value(Vector3 v3(1, 2, 3); vec.push_back(v3); ); it will double free(v3 free once, vec free second)
is this code available to check somewhere? I haven't quite understood the last part with delete with non trivial data types and variadic templates thing.
It is cool content! Thank you very much, Cherno!!! Like pressed! One thing I wish the vid had is a link to a git repo with code. Though, I am coding the data structure along with you, some place to look up code would not hinder (for educational purposes) 🤠
I'm getting a problem with PopBack. My editor/compiler isn't seeing 'T' as something valid within the scope of the function, and as such when I try to call the destructor on T, the next time that it tries to print the value from my Vector it's printing garbage (negative infinity). not sure what's wrong or how to fix this, I went back to the part in the video but couldn't figure out what I could have done wrong, and I'm not sure what to search for to figure out how to fix it
my code, for reference:
void PopBack()
{
if (m_Size > 0)
{
m_Size--;
m_Data[m_Size].~T();
}
}
Thanks for your video, I really enjoy all your series.
I have a question about T* newBlock = new T[newCapacity].
when it declared, why it shouldn't be deleted like the "delete[] newBlock" after "m_Capacity=newCapacity"??
Is that not leaking a memory ? if you do pushbacks 100 times, newBlock would stay 100 memory blocks in memory , am I incorrect?
Please teach me.
m_Data gets set to newBlock, so if you delete newBlock, you essentially delete m_Data, which is a big no no. You only need to delete the old data (m_Data) before you set m_Data to newBlock. After that both newBlock and m_Data point to the same memory address.
This is very strange timing because I just coded my own version of a vector class a few days ago. I'm curious how much different my implementation was from yours
Same, the Array episode inspired me to start a mystl project which I've been having some fun with.
Yeah, me too. I'm going to dive into the video now
Why not use smart pointers inside your vector class?
If you used realloc to make the allocation smaller, aren't you then leaking memory by forgetting the array contents you're now making out of range?
No. as you're copy/moving the elements to a new block of data, and then deleting the original data.
1. std::construct_at instead of operator=(T&&) in emplace (Edit: after some research I discovered std::construct_at is introduced in c++20, for c++17 and below, new() must be used)
2. you violated rule of 0/3/5 implement those ctors/ops! (TIL I learned there was a malloc/free counterpart in c++ in the form of ::operetor new/delete)
Anyone know why I am getting Copy Destroy twice before the 'Copy Copy Destroy Destroy Copy Destroy'? In debugging each time it pushes back a new object, Copy and Destroy get printed, so why is there only one showing in the video even though 2 objects are pushed back? Thanks.
new(&m_Data[m_Size]) T(std::forward(args)...); how cools is that 😂 that's awesome 😎 I didn't know that I can invoke new operator like that, thx ✌
By the way, examples like this are the best how to learn and understand how underlying std library works. 👍
That's called "placement new", and it is indeed really powerful! Have a look at memory pooling and custom allocation, the whole subject is quite interesting.
Visual Studio is free. Backend is powerful, but the front end user interface is archaic.
What :D Its the best. You must be from future.
hey Cherno, please add set class implementation using template, I am stuck at iterator logic implementation
42:08 I think we should use m_Capacity insted of m_Size when we clear our m_Data on reallocate memory. Does it ?
Capacity is how many elements can fit in allocated memory, while size is how many actual initialized elements there are. That means that there’s uninitialized memory (or remains of already destructed elements, e.g. if you called pop_back) outside of size range, and you really don’t wanna call dtor there
@@DeusAmentiam But, newCapacity can be greater than m_Capacity, m_Size in this situation can be greater than m_Capacity and for loop has been raise an error.
@Peter Konský Oh, I made a mistake. Thanks. But, i see another situation. newCapacity less that current m_Size and we are assign m_Size = newCapacity in this situation. Than we are call destructor not for all actual elements in our array. This is an error, does it ?
@Peter Konský Hah, we have a good point! :)
@SteamSablaBäst Yes, indeed
What are the benefits of overloading operators for a vector class? Wouldn't that make many things super confusing? Is V1 * V2 the Dot or the Cross-Product?
What do you guys think about OO?
Good point about using directly raw pointers in this type of cases (around the start of minute 15)! It got me thinking, just for the fun of understanding, how would a smart pointer (std's unique_ptr or shared_ptr) substitute the T* newBlock = new T[newCapacity]?
You can do it like this:
std:: unique_ptr newBlock = std::make_unique(newCapacity);
The advantage of this is it will call the correct version of the delete operator for you! However, I would still use raw pointers if I'm writing a custom container.
If the move assignment (which should really be a placement new to call the constructor) throws an exception, newBlock will not be deleted and you leak memory. A unique_ptr would prevent that. However, in that particular case, there should be a try-catch-block to ensure destructors are called on exactly those elements that have been succesfully move constructed. The more you think about it, the harder it gets to make this function bug free. In many other cases unique_ptr would be a great help, but not here.
Your ReAlloc function doesn't consider memory alignment
how so?
Now I have an excuse to sleep..
Data Strucure..
Since you no longer need to call constructors and destructors in the ReAlloc function, could you use memcpy instead of looping and copying each element one by one?
No, at least not always, suppose in your constructor you store the this pointer in some member variable. The memcpy would copy over the old/invalid pointer. I just ran into this.
I really enjoy exercises like these (std::tuple having been my favorite so far) and what i do is to go to cppreference and copy over the function signatures to be implemented later. this forces you to encounter things like custom allocators (which greatly improved my ability to optimize my code in larger projects) and learn how versatile templates can be.
In the case of vector i actually, rather than keeping track of sizes just kept track of a :
struct { struct { pointer begin, end; } allocation, elements; } m_data;
which has some time saving properties to it.
Don’t know the first thing about any of this but here I am.
When calling "T* new_block = ::operator new(new_capacity * sizeof(T));", I get a "new_block" that is only 8 bytes in length, i.e. "sizeof(new_block) == 8". Since our "Vector3" type is 32 bytes in length, I even tried being explicit about it and doing "T* new_block = ::operator new(64);" (allocating space for 2 Vector3 elements on first reallocation) just to help me debug. But even for that call, I still get a "new_block" that is 8 bytes long.
Am I doing something wrong when calling ::operator new()? Anyone have any idea why I'm getting the same size allocation regardless of what I pass to ::operator new()? Should we be using ::operator new[]() instead?
new_block is a pointer, when you do sizeof(pointer) you will get 4 for 32bit program or 8 for 64bit program
Anyone know why this line of code at the end of tutorial will not work:
Vector vector;
vector.PushBack("aca");
?
PushingBack string seams not to work after adding ::operator delete and new.
Replace this line
m_Data[m_Size] = std::move(value);
with
new(&m_Data[m_Size]) T(std::move(value));
void PushBack(T&& value)
{
if (m_Size >= m_Capacity)
Realloc(m_Capacity + m_Capacity / 2);
new(&m_Data[m_Size]) T(std::move(value));
m_Size++;
}
Will try it!