In this installment for Defaults and Deletes we finish the code for the example program. So far, among the things you have learned is that the default deconstructor does basically nothing. Near Heap allocations are reclaimed automatically when you return from a method/function. For Far Heap objects all it does is free the allocation. It will not free any sub allocations. You have already been shown the code for the ImplicitClass so we will not discuss it again.
NoFarHeap.h
/*
* nearheapclass.h
*
* Created on: Feb 1, 2024
* Author: roland
*/
#ifndef NOFARHEAP_H_
#define NOFARHEAP_H_
#include <optional>
class NoFarHeap
{
public:
NoFarHeap() = default;
explicit NoFarHeap( int i,
std::optional<double> dbl = std::nullopt,
std::optional<const char *> txt = std::nullopt);
NoFarHeap( const NoFarHeap &other) = default;
virtual ~NoFarHeap() = default;
int get_int();
double get_dbl();
char *get_buffer();
void set_int( int i);
void set_dbl( double d);
void set_buffer_content( char *txt);
private:
int m_int {25};
double m_dbl {3.14159}; // yes, I used PI
char m_buffer[2048] {'/*
* nearheapclass.h
*
* Created on: Feb 1, 2024
* Author: roland
*/
#ifndef NOFARHEAP_H_
#define NOFARHEAP_H_
#include <optional>
class NoFarHeap
{
public:
NoFarHeap() = default;
explicit NoFarHeap( int i,
std::optional<double> dbl = std::nullopt,
std::optional<const char *> txt = std::nullopt);
NoFarHeap( const NoFarHeap &other) = default;
virtual ~NoFarHeap() = default;
int get_int();
double get_dbl();
char *get_buffer();
void set_int( int i);
void set_dbl( double d);
void set_buffer_content( char *txt);
private:
int m_int {25};
double m_dbl {3.14159}; // yes, I used PI
char m_buffer[2048] {'\0'};
};
#endif /* NOFARHEAP_H_ */
'};
};
#endif /* NOFARHEAP_H_ */
I called this class NoFarHeap because it has no dynamic memory allocations. We took the compiler defaults for the default and copy constructor. If you read this post on std::optional then you recognize the only constructor I coded. The benefit of std::optional is it allows you to skip over parameters without knowing their default values and without having to have the coded in many places. It has drawbacks though.
NoFarHeap abc( 32, std::nullopt, "I want this text first");
NoFarHeap def( 27);
When the most frequent use case is just changing the first parameter of the class constructor, std::nullopt works great for the rest. As you can see, it is just cumbersome when you want to change first and last.
In case you do not know, at line 36, we declare m_buffer and initialize the entire thing to nulls. For this class I also allow the unsafe practice of returning the actual m_buffer pointer.
NearHeapClass.cpp
/*
* nearheapclass.cpp
*
* Created on: Feb 1, 2024
* Author: roland
*/
#include "nofarheap.h"
#include <cstring>
NoFarHeap::NoFarHeap(int i, std::optional<double> dbl, std::optional<const char *> txt) :
m_int(i)
{
if (dbl.has_value())
{
m_dbl = dbl.value();
}
if (txt.has_value())
{
strncpy( m_buffer, txt.value(), sizeof(m_buffer)-1);
}
}
int NoFarHeap::get_int()
{
return (m_int);
}
double NoFarHeap::get_dbl()
{
return (m_dbl);
}
char *NoFarHeap::get_buffer()
{
return (m_buffer);
}
void NoFarHeap::set_int(int i)
{
m_int = i;
}
void NoFarHeap::set_dbl(double d)
{
m_dbl = d;
}
void NoFarHeap::set_buffer_content(char *txt)
{
strncpy( m_buffer, txt, sizeof(m_buffer)-1);
}
Only thing worthy of note here is line 23 where we copy the text into buffer instead of changing the pointer.
FarHeapClass.h
/*
* dynamicclass.h
*
* Created on: Feb 1, 2024
* Author: roland
*/
#ifndef FARHEAPCLASS_H_
#define FARHEAPCLASS_H_
#include <string>
class FarHeapClass
{
public:
explicit FarHeapClass(int portNumber = 1, int bufferSize = 2048);
FarHeapClass(const FarHeapClass &other) = delete;
virtual ~FarHeapClass();
int buffer_size();
int port_number();
void fake_read_from_port();
std::string buffer_content();
FarHeapClass& operator=(const FarHeapClass&) = delete;
private:
int m_buffer_size;
int m_port_number;
char *m_buffer {};
};
#endif /* FARHEAPCLASS_H_ */
Pay close attention to line 17. I delete the copy constructor. Why did I do that? Line 32. This class dynamically allocates its buffer. It is also why line 27 deletes the assignment operator.
You should also pay attention to line 18. There is a growing academic trend of wanting all destructors to be declared virtual. I’m not on board with it, but it is a growing trend. The “philosophy” is that if someone derives a class from yours and is passing things around via a base class pointer then does a
delete ptr;
Only the base class destructor will be called/used. Putting virtual on the destructor forces the compiler to look up the actual class type and call the correct destructor.
FarHeapClass.cpp
/*
* dynamicclass.cpp
*
* Created on: Feb 1, 2024
* Author: roland
*/
#include <cstring>
#include <iostream>
#include "farheapclass.h"
FarHeapClass::FarHeapClass(int portNumber, int bufferSize) :
m_port_number(portNumber),
m_buffer_size(bufferSize)
{
m_buffer = new char [ m_buffer_size];
memset( m_buffer, '/*
* dynamicclass.cpp
*
* Created on: Feb 1, 2024
* Author: roland
*/
#include <cstring>
#include <iostream>
#include "farheapclass.h"
FarHeapClass::FarHeapClass(int portNumber, int bufferSize) :
m_port_number(portNumber),
m_buffer_size(bufferSize)
{
m_buffer = new char [ m_buffer_size];
memset( m_buffer, '\0', m_buffer_size);
}
FarHeapClass::~FarHeapClass()
{
if (m_buffer != nullptr)
{
delete m_buffer;
m_buffer = nullptr; // I like to do this just in case though it shouldn't matter
}
}
int FarHeapClass::buffer_size()
{
return (m_buffer_size);
}
int FarHeapClass::port_number()
{
return (m_port_number);
}
std::string FarHeapClass::buffer_content()
{
std::string str(m_buffer);
return (str); // returned by value
}
void FarHeapClass::fake_read_from_port()
{
if (m_buffer != nullptr)
{
std::string some_text;
std::cout << "=========== Please enter some text: ";
std::getline( std::cin, some_text);
strncpy(m_buffer, some_text.c_str(), m_buffer_size-1);
}
}
', m_buffer_size);
}
FarHeapClass::~FarHeapClass()
{
if (m_buffer != nullptr)
{
delete m_buffer;
m_buffer = nullptr; // I like to do this just in case though it shouldn't matter
}
}
int FarHeapClass::buffer_size()
{
return (m_buffer_size);
}
int FarHeapClass::port_number()
{
return (m_port_number);
}
std::string FarHeapClass::buffer_content()
{
std::string str(m_buffer);
return (str); // returned by value
}
void FarHeapClass::fake_read_from_port()
{
if (m_buffer != nullptr)
{
std::string some_text;
std::cout << "=========== Please enter some text: ";
std::getline( std::cin, some_text);
strncpy(m_buffer, some_text.c_str(), m_buffer_size-1);
}
}
Lines 16 and 17 are where we allocate our buffer and null it out. You will note that I do not let users get direct access to the buffer. Here we return a string.
While this class may seem hokey, I modeled it after many I’ve had to do in the embedded systems world. Usually for serial, but any other kind of port I/O. I just didn’t muddy the class up with platform specific port I/O code.
DefaultsAndDeletes.cpp
#include <iostream>
#include "config.h"
#include "implicitclass.h"
#include "nofarheap.h"
#include "farheapclass.h"
void dump_implicit( std::string label, ImplicitClass &ic)
{
std::cout << label << " fred: " << ic.fred << " ethyl: " << ic.ethyl
<< " status: " << ic.statusStr << std::endl;
}
void dump_noFarHeapClass( std::string label, NoFarHeap *ptr)
{
if (ptr != nullptr)
{
std::cout << "** " << label << " int: " << ptr->get_int()
<< " dbl: " << ptr->get_dbl()
<< " buffer: " << ptr->get_buffer() << std::endl;
}
}
void dump_farHeapClass( std::string label, FarHeapClass *ptr)
{
if (ptr != nullptr)
{
std::cout << "-- " << label << " buffer_size: " <<
ptr->buffer_size() << " port_number: " <<
ptr->port_number() << " content: " <<
ptr->buffer_content() << std::endl;
}
}
int main(int argc, char **argv) {
std::cout << "DefaultsAndDeletes" << std::endl;
std::cout << "Version " << DefaultsAndDeletes_VERSION_MAJOR << "." << DefaultsAndDeletes_VERSION_MINOR << std::endl;
std::cout << std::endl;
std::cout << ";;;;;;;;;;;;;;;;;;;" << std::endl << std::endl;
std::cout << " Implicit Class Examples" << std::endl << std::endl;
std::cout << ";;;;;;;;;;;;;;;;;;;" << std::endl << std::endl;
ImplicitClass lucy;
dump_implicit( "lucy", lucy);
ImplicitClass young {8, 9.6, "kids next door"};
dump_implicit( "young", young);
ImplicitClass ricki {lucy};
lucy = young; // be young again
dump_implicit( "second lucy", lucy);
dump_implicit( "ricky", ricki);
std::cout << std::endl;
std::cout << ";;;;;;;;;;;;;;;;;;;" << std::endl << std::endl;
std::cout << " NoFarHeap Class Examples" << std::endl << std::endl;
std::cout << ";;;;;;;;;;;;;;;;;;;" << std::endl << std::endl;
// this class doesn't dynamically allocate anything
//
NoFarHeap firstVar;
dump_noFarHeapClass( "firstVar: ", &firstVar);
NoFarHeap secondVar(32767, 98.5);
dump_noFarHeapClass( "secondVar: ", &secondVar);
// just because it doesn't use the heap doesn't mean it cannot be on
// the heap
//
NoFarHeap *thirdVar = new NoFarHeap(747, 3.33333, "Don't fly MAX");
dump_noFarHeapClass( "thirdVar: ", thirdVar);
*thirdVar = secondVar;
dump_noFarHeapClass( "thirdVar after assignment: ", thirdVar);
NoFarHeap fourthVar( *thirdVar);
dump_noFarHeapClass( "fourthVar: ", &fourthVar);
delete thirdVar;
std::cout << std::endl;
std::cout << ";;;;;;;;;;;;;;;;;;;" << std::endl << std::endl;
std::cout << " FarHeapClass Examples" << std::endl << std::endl;
std::cout << ";;;;;;;;;;;;;;;;;;;" << std::endl << std::endl;
FarHeapClass h1;
dump_farHeapClass( "Default constructor: ", &h1);
FarHeapClass *hp1 = new FarHeapClass( 2, 36000);
hp1->fake_read_from_port();
dump_farHeapClass( "After faking read: ", hp1);
delete hp1;
std::cout << std::endl << std::endl << "finished" << std::endl;
return (0);
}
Discussion
We will start our discussion at line 72 of the main module. Just because I called the class NoFarHeap doesn’t mean I can’t create one on the far heap via new.
Lines 81 and 95 where I delete the things I create need to be discussed. In fact, I’m going to re-use an image.
You are responsible for deleting everything you create. I work in the embedded systems world where we can’t let things consume memory because our stuff runs for weeks on end. I can also tell you that first bullet point is basically bullshit. If you are on OpenVMS or a mainframe it is true of every program no matter the language. Those platforms originated for time sharing and their accounting systems keeps track of every byte of RAM.
On the weaker feebler x86, Arm, etc. platforms, the operating systems are not robust. What they define as a process is too light to even be a thread on a real platform. When you allocate stuff inside threads or lambdas don’t expect it to go away when you exit.
Application Frameworks
I’ve written about Qt many times on here. I’ve pointed you to CopperSpice and wxWidgets as well. One of the big draws to these frameworks was garbage collection. Told you that in this post. Too many programmers were too lazy (or unskilled) when it came to memory management. They just assumed the operating system would take care of it and the machine ran slower and slower until it crashed. (Remind you of Microsoft?)
I ranted and ragged for years that the academics on the C++ standards committee needed to “fix new.” I wanted it to keep track of each allocation by file and line and timestamp when a program was compiled with debug and to print out a nice list of all things left not freed when execution ended. The academics never got around to it. They were more focused on doing stupid things like mandating 2’s complement for integer storage when Unysis mainframes (the ones used by the IRS and many other large financial institutions) are 1’s complement.
Industry got tired of waiting for academics to get a clue, it created AddressSanitizer. It got rolled into GNU and many other compilers around the time of Ubuntu 12.04. You can read more about it here and here. Some things you might want to keep handy for your CMake scripts.
CFLAGS += -fsanitize=address -fno-omit-frame-pointer
CXXFLAGS += -fsanitize=address -fno-omit-frame-pointer
LDFLAGS += -fno-omit-frame-pointer -fsanitize=address
ASAN_OPTIONS=strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1
Summary
When you have a very simple class like our ImplicitClass and NoFarHeap classes, take the defaults. It’s best to be explicit about them as we were in NoFarHeap. Everybody goes through that “I memorized the language standard” phase of coding, then they learn what it is to have a life and stop memorizing the language standard.
Any time you have a class that dynamically allocates something which must be freed in the destructor be certain to delete the copy constructor! If you don’t you will end up with a dual delete/free at runtime. If you think “smart pointers” will “solve” the issue for you read this post.
Making your destructor virtual does make it easier for someone to use it as a base class passing all of the classes they derived from it around via a base class pointer. It also adds vtable entries and overhead to the class at run-time. Unless you are an application framework created in the early 1990s basing all of your objects off something like QObject so you can ensure object deletion . . . should you really be doing that? This is 2024. We shouldn’t be cheating like that anymore, not without a damned good reason. In the 1990s we had to because neither the language nor the industry had tools like AddressSanitizer.
I’m not a fan of Template Metaprogramming, but it has its place. Most modern-ish C++ frameworks are going the route of making all containers templates so instead of having to use a QObject generic pointer the compiler spins up a customer container class for your object. Remember: STL == Standard Template Library.
Above all else remember that a default destructor does nothing. It’s just a free of the memory within your class object, not anything it allocated.