Browse Source

Using the new allocator in Array and Field collections.

David Piuva 9 months ago
parent
commit
cb922b05b8

+ 22 - 6
Source/DFPSR/api/stringAPI.cpp

@@ -67,14 +67,9 @@ static char toAscii(DsrChar c) {
 ReadableString::ReadableString(const DsrChar *content)
 : view(content, strlen_utf32(content)) {}
 
-ReadableString::ReadableString(const String& source)
-: characters(source.characters), view(source.view) {}
-
 String::String() {}
 String::String(const char* source) { atomic_append_ascii(*this, source); }
 String::String(const DsrChar* source) { atomic_append_utf32(*this, source); }
-String::String(const ReadableString& source)
-: ReadableString(source.characters, source.view) {}
 
 String& Printable::toStream(String& target) const {
 	return this->toStreamIndented(target, U"");
@@ -730,9 +725,30 @@ static uintptr_t getStartOffset(const ReadableString &source) {
 	return (start - origin) / sizeof(DsrChar);
 }
 
+#ifdef SAFE_POINTER_CHECKS
+	static void serializeCharacterBuffer(PrintCharacter target, void const * const allocation, uintptr_t maxLength) {
+		uintptr_t characterCount = heap_getUsedSize(allocation) / sizeof(DsrChar);
+		target(U'\"');
+		for (uintptr_t c = 0; c < characterCount; c++) {
+			if (c == maxLength) {
+				target(U'\"');
+				target(U'.');
+				target(U'.');
+				target(U'.');
+				return;
+			}
+			target(((DsrChar *)allocation)[c]);
+		}
+		target(U'\"');
+	}
+#endif
+
 static Handle<DsrChar> allocateCharacters(intptr_t minimumLength) {
 	// Allocate memory.
-	Handle<DsrChar> result = handle_createArray<DsrChar>(AllocationInitialization::Uninitialized, minimumLength);
+	Handle<DsrChar> result = handle_createArray<DsrChar>(AllocationInitialization::Uninitialized, minimumLength).setName("String characters");
+	#ifdef SAFE_POINTER_CHECKS
+		setAllocationSerialization(result.getUnsafe(), &serializeCharacterBuffer);
+	#endif
 	// Check how much space we got.
 	uintptr_t availableSpace = heap_getAllocationSize(result.getUnsafe());
 	// Expand to use all available memory in the allocation.

+ 56 - 12
Source/DFPSR/api/stringAPI.h

@@ -104,12 +104,7 @@ IMPL_ACCESS:
 	Handle<DsrChar> characters;
 	// Pointing to a subset of the buffer or memory that is not shared.
 	Impl_CharacterView view;
-	// TODO: Merge the pointer and length into a new View type for unified bound checks. Then remove the writer pointer.
-	//SafePointer<const DsrChar> reader;
-	//intptr_t length = 0;
 public:
-	// TODO: Inline the [] operator for faster reading of characters.
-	//       Use the padded read internally, because the old version was hard-coded for buffers padded to default alignment.
 	// Returning the character by value prevents writing to memory that might be a constant literal or shared with other strings
 	inline DsrChar operator[] (intptr_t index) const {
 		return this->view[index];
@@ -121,8 +116,35 @@ public:
 	ReadableString(const DsrChar *content);
 	ReadableString(Handle<DsrChar> characters, Impl_CharacterView view)
 	: characters(characters), view(view) {}
-	// Create from String by sharing the buffer
-	ReadableString(const String& source);
+	// Destructor.
+	~ReadableString() {}
+	// Copy constructor.
+	ReadableString(const ReadableString& source)
+	: characters(source.characters), view(source.view) {}
+	// Move constructor.
+	ReadableString(ReadableString &&source) noexcept
+	: characters(source.characters), view(source.view) {
+		source.characters = Handle<DsrChar>();
+		source.view = Impl_CharacterView();
+	}
+	// Copy assignment.
+	ReadableString& operator = (const ReadableString& source) {
+		if (this != &source) {
+			this->characters = source.characters;
+			this->view = source.view;
+		}
+		return *this;
+	};
+	// Move assignment.
+	ReadableString& operator = (ReadableString &&source) {
+		if (this != &source) {
+			this->characters = source.characters;
+			this->view = source.view;
+			source.characters = Handle<DsrChar>();
+			source.view = Impl_CharacterView();
+		}
+		return *this;
+	}
 };
 
 // A safe and simple string type
@@ -132,15 +154,37 @@ public:
 //     Endianness is native
 //     No combined characters allowed, use precomposed instead, so that the strings can guarantee a fixed character size
 class String : public ReadableString {
-//IMPL_ACCESS:
-	// TODO: Have a single pointer to the data in ReadableString and let the API be responsible for type safety.
-	//SafePointer<DsrChar> writer;
 public:
-	// Constructors
+	// Constructors.
 	String();
 	String(const char* source);
 	String(const DsrChar* source);
-	String(const ReadableString& source);
+	// Destructor.
+	~String() {}
+	// Copy constructor.
+	String(const ReadableString& source) : ReadableString(source) {}
+	String(const String& source) : ReadableString(source) {}
+	// Move constructor.
+	String(ReadableString &&source) noexcept : ReadableString(std::move(source)) {}
+	String(String &&source) noexcept : ReadableString(std::move(source)) {}
+	// Copy assignment.
+	String& operator = (const String& source) {
+		if (this != &source) {
+			this->characters = source.characters;
+			this->view = source.view;
+		}
+		return *this;
+	};
+	// Move assignment.
+	String& operator = (String &&source) {
+		if (this != &source) {
+			this->characters = source.characters;
+			this->view = source.view;
+			source.characters = Handle<DsrChar>();
+			source.view = Impl_CharacterView();
+		}
+		return *this;
+	}
 };
 
 // Used as format tags around numbers passed to string_append or string_combine

+ 119 - 8
Source/DFPSR/base/heap.cpp

@@ -23,6 +23,7 @@
 
 // TODO: Apply thread safety to more memory operations.
 //       heap_getUsedSize and heap_setUsedSize are often used together, forming a transaction without any mutex.
+//       But because of potential false sharing of cache lines, writing should be considered a transaction even if it is atomic.
 
 #include "../settings.h"
 #if defined(USE_MICROSOFT_WINDOWS)	
@@ -328,8 +329,7 @@ namespace dsr {
 		};
 		HeapDestructor destructor;
 		uintptr_t useCount = 0; // How many handles that point to the data.
-		// TODO: Allow the caller to access custom bit flags in allocations.
-		// uint32_t userFlags = 0;
+		uint32_t customFlags = 0; // Application defined allocation flags.
 		HeapFlag flags = 0; // Flags use the heapFlag_ prefix.
 		BinIndex binIndex = 0; // Recycling bin index to use when freeing the allocation.
 		HeapHeader(uintptr_t totalSize)
@@ -393,14 +393,14 @@ namespace dsr {
 	}
 
 	#ifdef SAFE_POINTER_CHECKS
-		void heap_setAllocationName(void * const allocation, const char *name) {
+		void heap_setAllocationName(void const * const allocation, const char *name) {
 			if (allocation != nullptr) {
 				HeapHeader *header = headerFromAllocation(allocation);
 				header->name = name;
 			}
 		}
 
-		const char * heap_getAllocationName(void * const allocation) {
+		const char * heap_getAllocationName(void const * const allocation) {
 			if (allocation == nullptr) {
 				return "none";
 			} else {
@@ -417,8 +417,83 @@ namespace dsr {
 				return memory_getPaddedSize_usingAndMask(header->getUsedSize(), heap_getHeapAlignmentAndMask());
 			}
 		}
+
+		void setAllocationSerialization(void const * const allocation, AllocationSerialization method) {
+			if (allocation != nullptr) {
+				HeapHeader *header = headerFromAllocation(allocation);
+				header->serializationMethod = method;
+			}
+		}
+		static void serializeUnknownData(PrintCharacter target, void const * const allocation, uintptr_t maxLength) {
+			uintptr_t byteCount = heap_getUsedSize(allocation);
+			target(U'{');
+			for (uintptr_t c = 0; c < byteCount; c++) {
+				uint8_t byteValue = ((uint8_t *)allocation)[c];
+				uint8_t firstHexValue = (byteValue >> 4) & 15;
+				uint8_t secondHexValue = byteValue       & 15;
+				if (c == maxLength) {
+					target(U'}');
+					target(U'.');
+					target(U'.');
+					target(U'.');
+					return;
+				}
+				if (c > 0) {
+					target(U' ');
+				}
+				if (firstHexValue < 10u) {
+					target(U'0' + (char32_t)firstHexValue);
+				} else {
+					target((U'A' - 10) + (char32_t)firstHexValue);
+				}
+				if (secondHexValue < 10u) {
+					target(U'0' + (char32_t)secondHexValue);
+				} else {
+					target((U'A' - 10) + (char32_t)secondHexValue);
+				}
+			}
+			target(U'}');
+		}
+		AllocationSerialization getAllocationSerialization(void const * const allocation) {
+			if (allocation == nullptr) {
+				return &serializeUnknownData;
+			} else {
+				HeapHeader *header = headerFromAllocation(allocation);
+				return header->serializationMethod != nullptr ? header->serializationMethod : &serializeUnknownData;
+			}
+		}
 	#endif
 
+	uint32_t heap_getAllocationCustomFlags(void const * const allocation) {
+		uint32_t result = 0;
+		if (allocation != nullptr) {
+			HeapHeader *header = headerFromAllocation(allocation);
+			result = ((HeapHeader *)header)->customFlags;
+		}
+		return result;
+	}
+
+	uint32_t heap_getAllocationCustomFlags(AllocationHeader * header) {
+		uint32_t result = 0;
+		if (header != nullptr) {
+			result = ((HeapHeader *)header)->customFlags;
+		}
+		return result;
+	}
+
+	void heap_setAllocationCustomFlags(void const * const allocation, uint32_t customFlags) {
+		if (allocation != nullptr) {
+			HeapHeader *header = headerFromAllocation(allocation);
+			((HeapHeader *)header)->customFlags = customFlags;
+		}
+	}
+
+	void heap_setAllocationCustomFlags(AllocationHeader * header, uint32_t customFlags) {
+		if (header != nullptr) {
+			((HeapHeader *)header)->customFlags = customFlags;
+		}
+	}
+
 	uintptr_t heap_getAllocationSize(AllocationHeader const * const header) {
 		uintptr_t result = 0;
 		if (header != nullptr) {
@@ -559,12 +634,14 @@ namespace dsr {
 	struct HeapPool {
 		HeapMemory *lastHeap = nullptr;
 		HeapHeader *recyclingBin[MAX_BIN_COUNT] = {};
-		void cleanUp() {
+		void cleanUp(bool noLeaks) {
 			// If memory safety checks are enabled, then we should indicate that everything is fine with the memory once cleaning up.
 			//   There is however no way to distinguish between leaking memory and not yet having terminated everything, so there is no leak warning to print.
 			#ifdef DSR_PRINT_NO_MEMORY_LEAK
 				// Can't allocate more memory after freeing all memory.
-				printf("All heap memory was freed without leaks.\n");
+				if (noLeaks) {
+					printf("All heap memory was freed without leaks.\n");
+				}
 			#endif
 			HeapMemory *nextHeap = this->lastHeap;
 			while (nextHeap != nullptr) {
@@ -708,7 +785,7 @@ namespace dsr {
 			allocationCount--;
 			// If the heap has been told to terminate and we reached zero allocations, we can tell it to clean up.
 			if (programState == ProgramState::Terminating && allocationCount == 0) {
-				defaultHeap.cleanUp();
+				defaultHeap.cleanUp(true);
 			}
 		unlockMemory();
 	}
@@ -740,7 +817,41 @@ namespace dsr {
 		// Then the arenas can safely be deallocated without looking at individual allocations again.
 		allocationCount = 0;
 		programState = ProgramState::Terminating;
-		defaultHeap.cleanUp();
+		defaultHeap.cleanUp(false);
+	}
+
+	static void printCharacter(char32_t character) {
+		if (character < 32) {
+			putchar(' ');
+		} else if (character > 127) {
+			putchar('?');
+		} else {
+			putchar((char)character);
+		}
+	}
+
+	// TODO: Can whole pointers be displayed using printf?
+	void heap_debugPrintAllocation(void const * const allocation, uintptr_t maxLength) {
+		uintptr_t size = (int)heap_getUsedSize(allocation);
+		#ifdef SAFE_POINTER_CHECKS
+			printf("* %s of %i bytes of use count %i at %p\n", heap_getAllocationName(allocation), (int)size, (int)heap_getUseCount(allocation), allocation);
+			printf("\t");
+			getAllocationSerialization(allocation)(printCharacter, allocation, maxLength);
+			printf("\n");
+		#else
+			printf("* Allocation of %i bytes of use count %i at %p\n", (int)size, (int)heap_getUseCount(allocation), allocation);
+		#endif
+	}
+
+	void heap_debugPrintAllocations(uintptr_t maxLength) {
+		printf("\nAllocations:\n");
+		heap_forAllHeapAllocations([maxLength](AllocationHeader * header, void * allocation) {
+			heap_debugPrintAllocation(allocation, maxLength);
+		});
+	}
+
+	intptr_t heap_getAllocationCount() {
+		return allocationCount;
 	}
 
 	void impl_throwAllocationFailure() {

+ 23 - 3
Source/DFPSR/base/heap.h

@@ -56,16 +56,19 @@
 #include <functional>
 
 namespace dsr {
-	// TODO: Replace with a lambda printing the name from capture and optional serialized content, because memory efficiency is not required in debug mode.
 	#ifdef SAFE_POINTER_CHECKS
 		// Assign a debug name to the allocation.
 		//   Does nothing if allocation is nullptr.
 		//   Only assign constant ascii string literals.
-		void heap_setAllocationName(void * const allocation, const char *name);
+		void heap_setAllocationName(void const * const allocation, const char *name);
 		// Get the ascii name of allocation, or "none" if allocation is nullptr.
-		const char * heap_getAllocationName(void * const allocation);
+		const char * heap_getAllocationName(void const * const allocation);
 		// Gets the size padded out to whole blocks of heap alignment, for constructing a SafePointer.
 		uintptr_t heap_getPaddedSize(void const * const allocation);
+		// Set the serialization function for a certain allocation, so that going through memory allocations will display its content when debugging memory leaks.
+		void setAllocationSerialization(void const * const allocation, AllocationSerialization method);
+		// Get the function used to interpret the allocation's content.
+		AllocationSerialization getAllocationSerialization(void const * const allocation);
 	#endif
 
 	// TODO: Allow allocating fixed size allocations using a typename and pre-calculate the bin index in compile time.
@@ -146,6 +149,11 @@ namespace dsr {
 	//   Recycled allocations are not included.
 	void heap_forAllHeapAllocations(std::function<void(AllocationHeader * header, void * allocation)> callback);
 
+	// Prints the allocation to the terminal.
+	void heap_debugPrintAllocation(void const * const allocation, uintptr_t maxLength = 128u);
+	// Prints a list of allocations to the terminal, without creating new memory allocations.
+	void heap_debugPrintAllocations(uintptr_t maxLength = 128u);
+
 	// Called by DSR_MAIN_CALLER when the program starts.
 	void heap_startingApplication();
 
@@ -155,6 +163,18 @@ namespace dsr {
 	// If terminating the program using std::exit, you can call this first to free all heap memory in the allocator, leaked or not.
 	void heap_hardExitCleaning();
 
+	// Used to find the origin of memory leaks in single-threaded tests.
+	intptr_t heap_getAllocationCount();
+
+	// Store application defined custom flags, which can be used for debugging memory leaks.
+	//   The flags do not take any additional memory, because an allocation head can not allocate less than a whole cache line.
+	uint32_t heap_getAllocationCustomFlags(void const * const allocation);
+	uint32_t heap_getAllocationCustomFlags(AllocationHeader * header);
+	void heap_setAllocationCustomFlags(void const * const allocation, uint32_t customFlags);
+	void heap_setAllocationCustomFlags(AllocationHeader * header, uint32_t customFlags);
+
+	// TODO: Allow storing custom information in allocation heads for debugging memory.
+
 	// Helper methods to prevent cyclic dependencies between strings and buffers when handles must be inlined for performance. Do not call these yourself.
 	void impl_throwAllocationFailure();
 	void impl_throwNullException();

+ 8 - 0
Source/DFPSR/base/memory.h

@@ -32,6 +32,12 @@
 #include <cstdint>
 
 namespace dsr {
+	#ifdef SAFE_POINTER_CHECKS
+		// Methods for slowly serializing the content of allocations without buffering with any dynamic memory.
+		using PrintCharacter = void(*)(char32_t character);
+		using AllocationSerialization = void(*)(PrintCharacter target, void const * const allocation, uintptr_t maxLength);
+	#endif
+
 	// A header that is placed next to memory allocations.
 	struct AllocationHeader {
 		uintptr_t totalSize; // Size of both header and payload.
@@ -42,6 +48,8 @@ namespace dsr {
 			const char *name = nullptr; // Debug name of the allocation.
 			uint64_t threadHash; // Hash of the owning thread identity for thread local memory, 0 for shared memory.
 			uint64_t allocationIdentity; // Rotating identity of the allocation, to know if the memory has been freed and reused within a memory allocator.
+			// A function for serialization.
+			AllocationSerialization serializationMethod = nullptr;
 		#endif
 		// Header for freed memory.
 		AllocationHeader();

+ 102 - 43
Source/DFPSR/collection/Array.h

@@ -1,7 +1,7 @@
 
 // zlib open source license
 //
-// Copyright (c) 2018 to 2019 David Forsgren Piuva
+// Copyright (c) 2018 to 2025 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -29,67 +29,126 @@
 
 namespace dsr {
 
-// A fixed size collection of elements initialized to the same default value.
+// A fixed size collection of impl_elements initialized to the same default value.
 //   Unlike Buffer, Array is a value type, so be careful not to pass it by value unless you intend to clone its content.
 template <typename T>
 class Array {
 private:
-	int64_t elementCount = 0;
-	T *elements = nullptr;
+	intptr_t impl_elementCount = 0;
+	T *impl_elements = nullptr;
+	void impl_free() {
+		if (this->impl_elements != nullptr) {
+			heap_decreaseUseCount(this->impl_elements);
+			this->impl_elements = nullptr;
+		}
+	}
+	// Pre-condition: this->impl_elements == nullptr
+	void impl_allocate(intptr_t elementCount) {
+		this->impl_elementCount = elementCount;
+		UnsafeAllocation newAllocation = heap_allocate(elementCount * sizeof(T), false);
+		#ifdef SAFE_POINTER_CHECKS
+			heap_setAllocationName(newAllocation.data, "Array allocation");
+		#endif
+		this->impl_elements = (T*)(newAllocation.data);
+		heap_increaseUseCount(newAllocation.header);
+	}
+	void impl_reallocate(intptr_t elementCount) {
+		// Check how much space is available in the target.
+		uintptr_t allocationSize = heap_getAllocationSize(this->impl_elements);
+		uintptr_t neededSize = elementCount * sizeof(T);
+		if (neededSize > allocationSize) {
+			// Need to replace the old allocation.
+			this->impl_free();
+			this->impl_allocate(elementCount);
+		} else {
+			// Resize the allocation within the available space.
+			heap_setUsedSize(this->impl_elements, neededSize);
+			this->impl_elementCount = elementCount;
+		}
+	}
+	void impl_destroy() {
+		for (intptr_t index = 0; index < this->impl_elementCount; index++) {
+			this->impl_elements[index].~T();
+		}
+	}
 public:
-	// Constructor
-	Array(const int64_t newLength, const T& defaultValue)
-	  : elementCount(newLength) {
-  		impl_nonZeroLengthCheck(newLength, "New array length");
-		this->elements = new T[newLength];
-		for (int64_t index = 0; index < newLength; index++) {
-			this->elements[index] = defaultValue;
+	// Constructors.
+	Array() : impl_elementCount(0), impl_elements(nullptr) {}
+	Array(const intptr_t newLength, const T& defaultValue) {
+		if (newLength > 0) {
+			this->impl_allocate(newLength);
+			for (intptr_t index = 0; index < newLength; index++) {
+				new (this->impl_elements + index) T(defaultValue);
+			}
+		} else {
+			this->impl_elementCount = 0;
 		}
 	}
-	// Clonable by default!
-	//   Be very careful not to accidentally pass an Array by value instead of reference,
-	//   otherwise your side-effects might write to a temporary copy
-	//   or time is wasted to clone an Array every time you look something up.
+	// Copy constructor.
 	Array(const Array<T>& source) {
-		// Allocate to the same size as source.
-		this->elements = new T[source.elementCount];
-		this->elementCount = source.elementCount;
-		// Copy elements from source.
-		for (int64_t e = 0; e < this->elementCount; e++) {
-			// Assign one element at a time, so that objects can be copy constructed.
-			//   If the element type T is trivial and does not require calling constructors, using safeMemoryCopy with SafePointer will be much faster than using Array<T>.
-			this->elements[e] = source.elements[e];
+		this->impl_allocate(source.impl_elementCount);
+		for (intptr_t e = 0; e < this->impl_elementCount; e++) {
+			new (this->impl_elements + e) T(source.impl_elements[e]);
 		}
 	};
-	// When assigning to the array, memory can be reused when the size is the same.
-	Array& operator=(const Array<T>& source) {
-		// Reallocate to the same size as source if needed.
-		if (this->elementCount != source.elementCount) {
-			if (this->elements) delete[] this->elements;
-			this->elements = new T[source.elementCount];
-		}
-		this->elementCount = source.elementCount;
-		// Copy elements from source.
-		for (int64_t e = 0; e < this->elementCount; e++) {
-			// Assign one element at a time, so that objects can be copy constructed.
-			//   If the element type T is trivial and does not require calling constructors, using safeMemoryCopy with SafePointer will be much faster than using Array<T>.
-			this->elements[e] = source.elements[e];
+	// Move constructor.
+	Array(Array<T> &&source) noexcept
+	: impl_elementCount(source.impl_elementCount), impl_elements(source.impl_elements) {
+		source.impl_elementCount = 0;
+		source.impl_elements = nullptr;
+	}
+	// Copy assignment.
+	Array<T>& operator = (const Array<T>& source) {
+		if (this != &source) {
+			this->impl_destroy();
+			this->impl_reallocate(source.impl_elementCount);
+			// Copy impl_elements from source.
+			for (intptr_t e = 0; e < this->impl_elementCount; e++) {
+				new (this->impl_elements + e) T(source.impl_elements[e]);
+			}
 		}
 		return *this;
 	};
+	// Move assignment.
+	Array<T>& operator = (Array<T> &&source) {
+		if (this != &source) {
+			this->impl_destroy();
+			this->impl_free();
+			this->impl_elementCount = source.impl_elementCount;
+			this->impl_elements = source.impl_elements;
+			source.impl_elementCount = 0;
+			source.impl_elements = nullptr;
+		}
+		return *this;
+	}
 	// Destructor
-	~Array() { if (this->elements) delete[] this->elements; }
+	~Array() {
+		this->impl_destroy();
+		this->impl_free();
+	}
+	// Bound check
+	inline bool inside(intptr_t index) const {
+		return 0 <= index && index < this->impl_elementCount;
+	}
+	inline T& unsafe_writeAccess(intptr_t index) {
+		assert(this->inside(index));
+		return this->impl_elements[index];
+	}
+	inline const T& unsafe_readAccess(intptr_t index) const {
+		assert(this->inside(index));
+		return this->impl_elements[index];
+	}
 	// Element access
-	T& operator[] (const int64_t index) {
+	T& operator[] (const intptr_t index) {
 		impl_baseZeroBoundCheck(index, this->length(), "Array index");
-		return this->elements[index];
+		return this->impl_elements[index];
 	}
-	const T& operator[] (const int64_t index) const {
+	const T& operator[] (const intptr_t index) const {
 		impl_baseZeroBoundCheck(index, this->length(), "Array index");
-		return this->elements[index];
+		return this->impl_elements[index];
 	}
-	int64_t length() const {
-		return this->elementCount;
+	inline intptr_t length() const {
+		return this->impl_elementCount;
 	}
 };
 

+ 45 - 43
Source/DFPSR/collection/Field.h

@@ -1,7 +1,7 @@
 
 // zlib open source license
 //
-// Copyright (c) 2018 to 2019 David Forsgren Piuva
+// Copyright (c) 2018 to 2025 David Forsgren Piuva
 // 
 // This software is provided 'as-is', without any express or implied
 // warranty. In no event will the authors be held liable for any damages
@@ -39,77 +39,79 @@ namespace dsr {
 template <typename T>
 class Field {
 private:
-	int64_t elementWidth = 0;
-	int64_t elementHeight = 0;
-	T *elements = nullptr;
+	Array<T> impl_elements;
+	intptr_t impl_elementWidth = 0;
+	intptr_t impl_elementHeight = 0;
 public:
-	// Constructor
-	Field(const int64_t width, const int64_t height, const T& defaultValue)
-	  : elementWidth(width), elementHeight(height) {
-		impl_nonZeroLengthCheck(width, "New array width");
-  		impl_nonZeroLengthCheck(height, "New array height");
-		int64_t size = width * height;
-		this->elements = new T[size];
-		for (int64_t index = 0; index < size; index++) {
-			this->elements[index] = defaultValue;
+	// Constructors
+	Field()
+	  : impl_elements(), impl_elementWidth(0), impl_elementHeight(0) {
+	}
+	Field(const intptr_t width, const intptr_t height, const T& defaultValue) {
+		if (width > 0 && height > 0) {
+			this->impl_elements = Array<T>(width * height, defaultValue);
+			this->impl_elementWidth = width;
+			this->impl_elementHeight = height;
 		}
 	}
 	// Bound check
-	bool inside(int64_t x, int64_t y) const {
-		return x >= 0 && x < this->elementWidth && y >= 0 && y < this->elementHeight;
+	inline bool inside(intptr_t x, intptr_t y) const {
+		return x >= 0 && x < this->impl_elementWidth && y >= 0 && y < this->impl_elementHeight;
 	}
 	// Direct memory access where bound checks are only applied in debug mode, so access out of bound will crash.
 	// Precondition: this->inside(x, y)
-	T& unsafe_writeAccess(int64_t x, int64_t y) {
+	inline T& unsafe_writeAccess(intptr_t x, intptr_t y) {
 		assert(this->inside(x, y));
-		return this->elements[x + y * this->elementWidth];
+		return this->impl_elements.unsafe_writeAccess(x + y * this->impl_elementWidth);
 	}
 	// Precondition: this->inside(x, y)
-	const T& unsafe_readAccess(int64_t x, int64_t y) const {
+	inline const T& unsafe_readAccess(intptr_t x, intptr_t y) const {
 		assert(this->inside(x, y));
-		return this->elements[x + y * this->elementWidth];
+		return this->impl_elements.unsafe_readAccess(x + y * this->impl_elementWidth);
 	}
+	/*
 	// Clonable by default!
 	//   Be very careful not to accidentally pass a Field by value instead of reference,
 	//   otherwise your side-effects might write to a temporary copy
 	//   or time is wasted to clone an Field every time you look something up.
 	Field(const Field<T>& source) {
 		// Allocate to the same size as source.
-		int64_t newSize = source.elementWidth * source.elementHeight;
-		this->elements = new T[newSize];
-		this->elementWidth = source.elementWidth;
-		this->elementHeight = source.elementHeight;
+		intptr_t newSize = source.impl_elementWidth * source.impl_elementHeight;
+		this->impl_elements = new T[newSize];
+		this->impl_elementWidth = source.impl_elementWidth;
+		this->impl_elementHeight = source.impl_elementHeight;
 		// Copy elements from source.
-		for (int64_t e = 0; e < newSize; e++) {
+		for (intptr_t e = 0; e < newSize; e++) {
 			// Assign one element at a time, so that objects can be copy constructed.
 			//   If the element type T is trivial and does not require calling constructors, using safeMemoryCopy with SafePointer will be much faster than using Array<T>.
-			this->elements[e] = source.elements[e];
+			this->impl_elements[e] = source.impl_elements[e];
 		}
 	};
 	// When assigning to the field, memory can be reused when the number of elements is the same.
 	Field& operator=(const Field<T>& source) {
-		int64_t oldSize = this->elementWidth * this->elementHeight;
-		int64_t newSize = source.elementWidth * source.elementHeight;
+		intptr_t oldSize = this->impl_elementWidth * this->impl_elementHeight;
+		intptr_t newSize = source.impl_elementWidth * source.impl_elementHeight;
 		// Reallocate to the same size as source if needed.
 		if (oldSize != newSize) {
-			if (this->elements) delete[] this->elements;
-			this->elements = new T[newSize];
+			if (this->impl_elements) delete[] this->impl_elements;
+			this->impl_elements = new T[newSize];
 		}
 		// Update dimensions, even if the combined allocation size is the same.
-		this->elementWidth = source.elementWidth;
-		this->elementHeight = source.elementHeight;
+		this->impl_elementWidth = source.impl_elementWidth;
+		this->impl_elementHeight = source.impl_elementHeight;
 		// Copy elements from source.
-		for (int64_t e = 0; e < newSize; e++) {
+		for (intptr_t e = 0; e < newSize; e++) {
 			// Assign one element at a time, so that objects can be copy constructed.
 			//   If the element type T is trivial and does not require calling constructors, using safeMemoryCopy with SafePointer will be much faster than using Array<T>.
-			this->elements[e] = source.elements[e];
+			this->impl_elements[e] = source.impl_elements[e];
 		}
 		return *this;
 	};
+	*/
 	// Destructor
-	~Field() { if (this->elements) delete[] this->elements; }
+	//~Field() { if (this->impl_elements) delete[] this->impl_elements; }
 	// Get the element at (x, y) or the outside value when (x, y) is out-of-bound.
-	T read_border(int64_t x, int64_t y, const T& outside) const {
+	T read_border(intptr_t x, intptr_t y, const T& outside) const {
 		if (this->inside(x, y)) {
 			return this->unsafe_readAccess(x, y);
 		} else {
@@ -117,24 +119,24 @@ public:
 		}
 	}
 	// Get the element closest to (x, y), by clamping the coordinate to valid bounds.
-	T read_clamp(int64_t x, int64_t y) const {
+	T read_clamp(intptr_t x, intptr_t y) const {
 		if (x < 0) x = 0;
-		if (x >= this->elementWidth) x = this->elementWidth - 1;
+		if (x >= this->impl_elementWidth) x = this->impl_elementWidth - 1;
 		if (y < 0) y = 0;
-		if (y >= this->elementHeight) y = this->elementHeight - 1;
+		if (y >= this->impl_elementHeight) y = this->impl_elementHeight - 1;
 		return this->unsafe_readAccess(x, y);
 	}
 	// Write value to the element at (x, y) when inside of the bounds, ignoring the operation silently when outside.
-	void write_ignore(int64_t x, int64_t y, const T& value) {
+	void write_ignore(intptr_t x, intptr_t y, const T& value) {
 		if (this->inside(x, y)) {
 			this->unsafe_writeAccess(x, y) = value;
 		}
 	}
-	int64_t width() const {
-		return this->elementWidth;
+	inline intptr_t width() const {
+		return this->impl_elementWidth;
 	}
-	int64_t height() const {
-		return this->elementHeight;
+	inline intptr_t height() const {
+		return this->impl_elementHeight;
 	}
 
 	// Wrappers for access using UVector instead of separate (x, y) coordinates.

+ 3 - 0
Source/DFPSR/collection/List.h

@@ -51,6 +51,9 @@ private:
 		if (minimumAllocatedLength > this->impl_buffer_length) {
 			// Create a new memory allocation.
 			UnsafeAllocation newAllocation = (heap_allocate(minimumAllocatedLength * sizeof(T), true));
+			#ifdef SAFE_POINTER_CHECKS
+				heap_setAllocationName(newAllocation.data, "List allocation");
+			#endif
 			T *newElements = (T*)(newAllocation.data);
 			heap_increaseUseCount(newAllocation.header);
 			// Use all available space.

+ 39 - 16
Source/test/testTools.h

@@ -6,6 +6,7 @@
 #include "../DFPSR/math/FVector.h"
 #include <cstdlib>
 #include <csignal>
+#include <stdio.h>
 
 using namespace dsr;
 
@@ -79,24 +80,31 @@ static bool failed = false;
 
 #define START_TEST(NAME) \
 DSR_MAIN_CALLER(dsrMain) \
-void dsrMain(List<String> args) { \
-	testName = #NAME; \
-	stateName = U"While Assigning message handler"; \
-	std::signal(SIGSEGV, [](int signal) { failed = true; throwError(U"Segmentation fault from ", testName, U"! ", stateName); }); \
-	string_assignMessageHandler(&messageHandler); \
-	stateName = U"While handling arguments\n"; \
-	handleArguments(args); \
-	stateName = U"Test start\n"; \
-	printText(U"Running test \"", #NAME, "\":\n ");
+void dsrMain(List<String> args) \
+	{ \
+		testName = #NAME; \
+		stateName = U"While Assigning message handler"; \
+		std::signal(SIGSEGV, [](int signal) { failed = true; throwError(U"Segmentation fault from ", testName, U"! ", stateName); }); \
+		string_assignMessageHandler(&messageHandler); \
+		stateName = U"While handling arguments\n"; \
+		handleArguments(args); \
+		stateName = U"Test start\n"; \
+		printText(U"Running test \"", #NAME, "\":\n "); \
+		intptr_t startAllocationCount = heap_getAllocationCount(); \
+		heap_forAllHeapAllocations([](AllocationHeader * header, void * allocation) { \
+			heap_setAllocationCustomFlags(header, 1u); \
+		}); \
+		{
 
 #define END_TEST \
-	printText(U" (done)\n"); \
-	stateName = U"After test end\n"; \
-	if (failed) { \
-		heap_terminatingApplication(); \
-		exit(1); \
-	} \
-}
+		} \
+		printText(U" (done)\n"); \
+		stateName = U"After test end\n"; \
+		if (failed) { \
+			heap_terminatingApplication(); \
+			exit(1); \
+		} \
+	}
 
 // These can be used instead of ASSERT_CRASH to handle multiple template arguments that are not enclosed within ().
 #define BEGIN_CRASH(PREFIX) \
@@ -157,6 +165,21 @@ void dsrMain(List<String> args) { \
 #define ASSERT_GREATER_OR_EQUAL(A, B) ASSERT_COMP(A, B, OP_GREATER_OR_EQUAL, ">=")
 #define ASSERT_NEAR(A, B) ASSERT_COMP(A, B, OP_NEAR, "==")
 
+#define ASSERT_HEAP_DEPTH(DEPTH) { \
+	intptr_t currentAllocationCount = heap_getAllocationCount(); \
+	intptr_t expectedAllocationCount = startAllocationCount + (DEPTH); \
+	if (currentAllocationCount != expectedAllocationCount) { \
+		printf( \
+			"\n\n" \
+			"_______________________________ FAIL _______________________________\n" \
+			"\n" \
+			"Expected allocation count %i but found %i allocations instead.\n" \
+			"____________________________________________________________________\n" \
+		, (int)expectedAllocationCount, (int)currentAllocationCount); \
+		failed = true; \
+	} \
+}
+
 const dsr::String inputPath = dsr::string_combine(U"test", file_separator(), U"input", file_separator());
 const dsr::String expectedPath = dsr::string_combine(U"test", file_separator(), U"expected", file_separator());
 

+ 49 - 2
Source/test/tests/ArrayTest.cpp

@@ -3,8 +3,10 @@
 #include "../../DFPSR/collection/Array.h"
 
 START_TEST(Array)
-	{
+	{ // Arrays of integers.
+		ASSERT_HEAP_DEPTH(0);
 		Array<int> a = Array<int>(4, 123);
+		ASSERT_HEAP_DEPTH(1);
 		a[1] = 85;
 		a[3] = -100;
 		ASSERT_EQUAL(a.length(), 4);
@@ -12,6 +14,7 @@ START_TEST(Array)
 		ASSERT_EQUAL(a[1], 85);
 		ASSERT_EQUAL(a[2], 123);
 		ASSERT_EQUAL(a[3], -100);
+		ASSERT_HEAP_DEPTH(1);
 		ASSERT_EQUAL(string_combine(a),
 		  U"{\n"
 		  U"	123,\n"
@@ -20,10 +23,13 @@ START_TEST(Array)
 		  U"	-100\n"
 		  U"}"
 		);
+		ASSERT_HEAP_DEPTH(1);
 		// An initial assignment uses the copy constructor, because there is no pre-existing data in b.
 		Array<int> b = a;
+		ASSERT_HEAP_DEPTH(2);
 		b[0] = 200;
 		b[2] = 100000;
+		ASSERT_HEAP_DEPTH(2);
 		// The b array has changed...
 		ASSERT_EQUAL(string_combine(b),
 		  U"{\n"
@@ -33,6 +39,7 @@ START_TEST(Array)
 		  U"	-100\n"
 		  U"}"
 		);
+		ASSERT_HEAP_DEPTH(2);
 		// ...but a remains the same, because the data was cloned when assigning.
 		ASSERT_EQUAL(string_combine(a),
 		  U"{\n"
@@ -44,12 +51,15 @@ START_TEST(Array)
 		);
 		// They are not equal
 		ASSERT_NOT_EQUAL(a, b);
-		// Assigning from copy construction is optimized into an assignment operation, because b already exists.
+		ASSERT_HEAP_DEPTH(2);
+		// Assigning from copy construction.
 		a = Array<int>(b);
+		ASSERT_HEAP_DEPTH(2);
 		// Now they are equal
 		ASSERT_EQUAL(a, b);
 		// Create another length
 		Array<int> c = Array<int>(7, 75);
+		ASSERT_HEAP_DEPTH(3);
 		ASSERT_EQUAL(string_combine(c),
 		  U"{\n"
 		  U"	75,\n"
@@ -63,6 +73,7 @@ START_TEST(Array)
 		);
 		// Assign larger array
 		a = c;
+		ASSERT_HEAP_DEPTH(3);
 		ASSERT_EQUAL(string_combine(a),
 		  U"{\n"
 		  U"	75,\n"
@@ -78,6 +89,7 @@ START_TEST(Array)
 		ASSERT_NOT_EQUAL(a, b);
 		// Assign smaller array
 		c = b;
+		ASSERT_HEAP_DEPTH(3);
 		ASSERT_EQUAL(string_combine(c),
 		  U"{\n"
 		  U"	200,\n"
@@ -89,4 +101,39 @@ START_TEST(Array)
 		ASSERT_EQUAL(c, b);
 		ASSERT_NOT_EQUAL(a, c);
 	}
+	{ // Arrays of non-trivial types.
+		Array<Array<String>> a = Array<Array<String>>(2, Array<String>(3, U"?"));
+		ASSERT_EQUAL(a.length(), 2);
+		ASSERT_CRASH(a[-1], U"Array index -1 is out of bound 0..1!");
+		ASSERT_EQUAL(a[0].length(), 3);
+		ASSERT_EQUAL(a[1].length(), 3);		
+		ASSERT_CRASH(a[2], U"Array index 2 is out of bound 0..1!");
+		ASSERT_EQUAL(a[0][0], U"?");
+		ASSERT_EQUAL(a[0][1], U"?");
+		ASSERT_EQUAL(a[0][2], U"?");		
+		ASSERT_EQUAL(a[1][0], U"?");
+		ASSERT_EQUAL(a[1][1], U"?");
+		ASSERT_EQUAL(a[1][2], U"?");
+		a[0][0] = U"Testing";
+		a[0][1] = U"an";
+		a[0][2] = U"array";
+		a[1][0] = U"of";
+		a[1][1] = U"string";
+		a[1][2] = U"arrays";
+		ASSERT_EQUAL(a[0][0], U"Testing");
+		ASSERT_EQUAL(a[0][1], U"an");
+		ASSERT_EQUAL(a[0][2], U"array");		
+		ASSERT_EQUAL(a[1][0], U"of");
+		ASSERT_EQUAL(a[1][1], U"string");
+		ASSERT_EQUAL(a[1][2], U"arrays");
+		Array<String> b = std::move(a[0]);
+		ASSERT_EQUAL(a[0].length(), 0);
+		ASSERT_EQUAL(a[1].length(), 3);
+		ASSERT_EQUAL(b.length(), 3);
+		Array<String> c(std::move(a[1]));
+		ASSERT_EQUAL(a[0].length(), 0);
+		ASSERT_EQUAL(a[1].length(), 0);
+		ASSERT_EQUAL(b.length(), 3);
+		ASSERT_EQUAL(c.length(), 3);
+	}
 END_TEST