Insinuating that OOP is bad? You insult me sir.
Besides which the correct answer is to burn “Thinking in C++” and replace it with a better book.
All this using namespace std;
and fputs
is ugly outdated nonsense.
I can’t reproduce any infinite looping problems,
but I can see why you’re getting “All AutoCounter object not cleaned up”.
What’s actually happening is that AutoCounter::~AutoCounter()
is never called because the AutoCounter
object is never destroyed,
because you new
the object and then never delete
it!
I.e. a memory leak.
But really this whole class is inadequate anyway because it’s trying to use pointers (AutoCounter *
) to track objects,
but objects don’t necessarily occupy the same address for the whole program.
They often get copied and moved around, e.g. pushed into a std::vector
.
A proper implementation of a reference counter has to take creation, destruction, copy-construction, move-construction, copy-assignment and move-assignment into account.
E.g. something more like this:
#pragma once
// Needed for std::size_t
#include <cstddef>
#include <exception>
#include <iostream>
class ReferenceCounter
{
public:
using counter_type = std::size_t;
private:
counter_type count = 0;
public:
ReferenceCounter()
{
// Console output used for debug purposes only
std::cout << "A reference counter is initialised" << '\n';
}
~ReferenceCounter()
{
// Console output used for debug purposes only
if(count > 0)
std::cout << "A reference wasn't properly cleaned up" << '\n';
else
std::cout << "All references correctly cleaned up" << '\n';
}
counter_type get_count() const
{
return this->count;
}
void add_reference()
{
++count;
}
void remove_reference()
{
if(count == 0)
throw std::exception("Attempt to remove a reference from an exhausted counter");
--count;
}
};
template< typename Type >
class ReferenceSentry
{
public:
using type = Type;
using counter_type = std::size_t;
private:
static ReferenceCounter counter;
public:
static const ReferenceCounter & get_counter()
{
return counter;
}
private:
bool has_reference = false;
private:
void acquire_reference()
{
if(this->has_reference)
throw std::exception("Attempt to reaquire reference");
counter.add_reference();
this->has_reference = true;
}
void release_reference()
{
if(!this->has_reference)
throw std::exception("Attempt to rerelease reference");
counter.remove_reference();
this->has_reference = false;
}
public:
// Construction adds a reference
ReferenceSentry()
{
this->acquire_reference();
}
// Destruction removes a reference
~ReferenceSentry()
{
if(this->has_reference)
this->release_reference();
}
// Copying acquires and releases as necessary
ReferenceSentry(const ReferenceSentry & other)
{
if(other.has_reference)
{
if(!this->has_reference)
this->acquire_reference();
}
else
{
if(this->has_reference)
this->release_reference();
}
}
// Moving behaves almost the same as copying for this type
ReferenceSentry(ReferenceSentry && other)
{
if(other.has_reference)
{
if(!this->has_reference)
this->acquire_reference();
other.release_reference();
}
else
{
if(this->has_reference)
this->release_reference();
}
}
// Copying acquires and releases as necessary
ReferenceSentry & operator=(const ReferenceSentry & other)
{
if(other.has_reference)
{
if(!this->has_reference)
this->acquire_reference();
}
else
{
if(this->has_reference)
this->release_reference();
}
return *this;
}
// Moving behaves almost the same as copying for this type
ReferenceSentry & operator=(ReferenceSentry && other)
{
if(other.has_reference)
{
if(!this->has_reference)
this->acquire_reference();
other.release_reference();
}
else
{
if(this->has_reference)
this->release_reference();
}
return *this;
}
};
template<typename T>
ReferenceCounter ReferenceSentry<T>::counter = ReferenceCounter();
And as proof that this works (for at least the few cases that I’ve tried):
#include <iostream>
#include "ReferenceCounter.h"
class Resource
{
public:
using sentry_type = ReferenceSentry<Resource>;
private:
sentry_type sentry;
public:
const sentry_type & get_sentry() const
{
return this->sentry;
}
};
int main()
{
using sentry_type = ReferenceSentry<Resource>;
std::cout << sentry_type::get_counter().get_count() << '\n';
{
Resource resourceA;
std::cout << sentry_type::get_counter().get_count() << '\n';
Resource resourceB;
std::cout << sentry_type::get_counter().get_count() << '\n';
// Copying does not affect the counter because it merely copies resourceA
resourceB = resourceA;
std::cout << sentry_type::get_counter().get_count() << '\n';
// Moving decreases the counter because it destroys resourceA
resourceB = std::move(resourceA);
std::cout << sentry_type::get_counter().get_count() << '\n';
// resourceA is already destroyed, so trying to construct from it does not increase the counter
Resource resourceC(resourceA);
std::cout << sentry_type::get_counter().get_count() << '\n';
}
// The scope ends and all resources are destroyed
std::cout << sentry_type::get_counter().get_count() << '\n';
std::cin.get();
}
Showing that my ResourceSentry
class can be used as a per-type counter that can be placed as a member object inside any class to enable reference counting.
Perhaps that’s a few features short of what AutoCounter
is supposed to do,
but to do what AutoCounter
is supposed to do without any flaws is somewhat more difficult because (as mentioned before) objects move around in memory so you can’t simply trace them by tracing their addresses, you’d need a way to generate unique object ids for every object, which gets pretty messy and frankly is quite an unrealistic requirement anyway.
In this modern day and age you don’t really need to be able to write something like that anyway.
std::shared_ptr
, std::unique_ptr
and std::weak_ptr
are almost all you’ll ever need for managing resources.
You can even use them to wrap C library resources by providing a custom destructor.
For example, wrapping an SDL_Window
:
std::shared_ptr<SDL_Window> createWindow(const char * title, int x, int y, int w, int h, Uint32 flags)
{
return std::shared_ptr<SDL_Window>(SDL_CreateWindow(title, x, y, w, h, flags), SDL_DestroyWindow);
}
Which ensures that the window is destroyed when no shared pointers are still pointing to it.
I’m not saying you should never learn how to write a reference counter (I actually wrote one within my first year or so of learning C++),
but this book’s AutoCounter
doesn’t seem to be a particularly good example of how to do it.
Also most sane programs don’t use std::exit
.
These days errors are signalled by throwing exceptions.
std::exit
is for when you’ve designed yourself into a hole and you can’t exit in a nice way.
(There’s a good SO answer about that here.)