8
\$\begingroup\$

I've been interested in writing something meaningful in C++ for a long time, yet I had a hard time not picking Java or C# for a new project... Now I've found something for which C++ seems to be the right tool: Writing a Database Management System.

I'm going to show you the code for the first step: Storing databases.

The code is written in Microsoft Visual C++ 17 and I'm trying to have code that is as modern as possible. I also didn't intend to build a serialization library, it just happened. This library exports its methods through a DLL.


stdafx.h

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <windows.h>

#include <string>
#include <vector>
#include <fstream>
#include <filesystem>
#include <optional>

#ifdef DBMS_EXPORTS
#define DBMS_EXPORTS_API __declspec(dllexport)
#else
#define DBMS_EXPORTS_API __declspec(dllimport)
#endif

targetver.h

#pragma once

// Including SDKDDKVer.h defines the highest available Windows platform.

// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.

#include <SDKDDKVer.h>

DBMS.h

#pragma once

#include "stdafx.h"
#include "Database.h"

namespace DBMS
{    
    class DBMS
    {
    public:
        DBMS_EXPORTS_API DBMS(const std::string file);

        DBMS_EXPORTS_API Database& CreateDatabase(const std::string file, const std::string name);

        DBMS_EXPORTS_API std::optional<std::reference_wrapper<const Database>> GetDatabaseByFile(const std::string file) const;

        DBMS_EXPORTS_API std::optional<std::reference_wrapper<const Database>> GetDatabaseByName(const std::string name) const;

        DBMS_EXPORTS_API void DeleteDatabase(const Database& database);

    private:
        const std::string file;
        std::vector<Database> databases;

        void SaveToDisk() const;
    };
}

Database.h

#pragma once

#include "stdafx.h"

namespace DBMS
{
    class Database
    {
    public:
        DBMS_EXPORTS_API Database(const std::string file, const std::string name);

        DBMS_EXPORTS_API std::string GetFile() const;

        DBMS_EXPORTS_API std::string GetName() const;

        DBMS_EXPORTS_API bool operator==(const Database& other) const;

        DBMS_EXPORTS_API bool operator!=(const Database& other) const;

    private:
        std::string file;
        std::string name;
    };
}

Serializer.h

#pragma once

#include "stdafx.h"
#include "Database.h"
#include <iostream>

namespace DBMS
{
    template <typename T>
    class Serializer
    {
    public:
        static void Serialize(std::ofstream& out, const T& elem)
        {
            static_assert(std::is_trivially_copyable_v<T>, "standard template is only defined for trivially copyable values");
            static_assert(!std::is_pointer_v<T>, "standard template is not defined for pointers of trivially copyable values");
            static_assert(!std::is_array_v<T>, "standard template is not defined for arrays of trivially copyable values");
            out.write(reinterpret_cast<const char*>(&elem), sizeof(T));
        }

        static T Deserialize(std::ifstream& in)
        {
            static_assert(std::is_trivially_copyable_v<T>, "standard template is only defined for trivially copyable values");
            T elem;
            in.read(reinterpret_cast<char*>(&elem), sizeof(T));
            return elem;
        }
    };

    template <typename T>
    class Serializer<T*>
    {
    public:
        static void Serialize(std::ofstream& out, const T* const& elem, size_t length)
        {
            static_assert(std::is_trivially_copyable_v<T>, "standard template is only defined for trivially copyable values");
            out.write(reinterpret_cast<const char*>(elem), sizeof(T) * length);
        }

        static T* Deserialize(std::ifstream& in, size_t length)
        {
            static_assert(std::is_trivially_copyable_v<T>, "standard template is only defined for trivially copyable values");
            auto elements = std::make_unique<T[]>(length);
            T* raw = elements.get();
            in.read(reinterpret_cast<char*>(raw), sizeof(T) * length);
            elements.release();
            return raw;
        }
    };

    template <typename E>
    class Serializer<std::vector<E>>
    {
    public:
        static void Serialize(std::ofstream& out, const std::vector<E>& vector)
        {
            Serializer<uint32_t>::Serialize(out, vector.size());

            for (const E& elem : vector)
            {
                Serializer<E>::Serialize(out, elem);
            }
        }

        static std::vector<E> Deserialize(std::ifstream& in)
        {
            auto size = Serializer<uint32_t>::Deserialize(in);
            auto vector = std::vector<E>();
            vector.reserve(size);

            for (uint32_t i = 0; i < size; i++)
            {
                vector.push_back(Serializer<E>::Deserialize(in));
            }

            return vector;
        }
    };

    template <>
    class Serializer<std::string>
    {
    public:
        static void Serialize(std::ofstream& out, const std::string& string)
        {
            Serializer<uint32_t>::Serialize(out, string.length());
            Serializer<const char*>::Serialize(out, string.data(), string.length() + 1);
        }

        static std::string Deserialize(std::ifstream& in)
        {
            auto length = Serializer<uint32_t>::Deserialize(in);
            auto raw = Serializer<char*>::Deserialize(in, length + 1);
            return std::string(raw, length + 1);
        }
    };

    template <>
    class Serializer<Database>
    {
    public:
        static void Serialize(std::ofstream& out, const Database& database)
        {
            Serializer<std::string>::Serialize(out, database.GetFile());
            Serializer<std::string>::Serialize(out, database.GetName());
        }

        static Database Deserialize(std::ifstream& in)
        {
            auto file = Serializer<std::string>::Deserialize(in);
            auto name = Serializer<std::string>::Deserialize(in);
            return Database(file, name);
        }
    };
}

DBMS.cpp

// DBMS.cpp : Defines the exported functions for the DLL application.
//

#include "stdafx.h"
#include "Database.h"
#include "Serializer.h"

namespace DBMS
{
    class DBMS
    {
    public:
        DBMS_EXPORTS_API DBMS(const std::string file) : file(file)
        {
            if (std::experimental::filesystem::exists(file))
            {
                std::ifstream fs(file, std::ios::binary);
                if (fs.is_open())
                {
                    databases = Serializer<std::vector<Database>>::Deserialize(fs);
                }
            }
            else
            {
                // save empty database to disk
                SaveToDisk();
            }
        }

        DBMS_EXPORTS_API Database& CreateDatabase(const std::string file, const std::string name)
        {
            Database database(file, name);
            databases.push_back(database);
            SaveToDisk();
            return databases.back();
        }

        DBMS_EXPORTS_API std::optional<std::reference_wrapper<const Database>> GetDatabaseByFile(const std::string file) const
        {
            for (auto& database : databases)
            {
                if (database.GetFile() == file)
                {
                    return database;
                }
            }
            return std::optional<std::reference_wrapper<const Database>>();
        }

        DBMS_EXPORTS_API std::optional<std::reference_wrapper<const Database>> GetDatabaseByName(const std::string name) const
        {
            for (auto& database : databases)
            {
                if (database.GetName() == name)
                {
                    return database;
                }
            }
            return std::optional<std::reference_wrapper<const Database>>();
        }

        DBMS_EXPORTS_API void DeleteDatabase(const Database& database)
        {
            for (const auto& db : databases)
            {
                if (db == database)
                {
                    auto it = std::find(databases.begin(), databases.end(), database);
                    if (it != databases.end())
                    {
                        databases.erase(it);
                    }
                }
            }
            SaveToDisk();
        }

    private:
        const std::string file;
        std::vector<Database> databases;

        void SaveToDisk() const
        {
            std::ofstream fs(file, std::ios::binary, std::ios::trunc);   // trunc is a dirty hack to not have to be smart
            if (fs.is_open())
            {
                Serializer<std::vector<Database>>::Serialize(fs, databases);
            }
        }
    };
}

Database.cpp

#include "stdafx.h"

namespace DBMS
{
    class Database
    {
    public:
        DBMS_EXPORTS_API Database(const std::string file, const std::string name) : file(file), name(name)
        {

        }

        DBMS_EXPORTS_API std::string GetFile() const
        {
            return file;
        }

        DBMS_EXPORTS_API std::string GetName() const
        {
            return name;
        }

        DBMS_EXPORTS_API bool operator==(const Database& other) const
        {
            return (file == other.file && name == other.name);
        }

        DBMS_EXPORTS_API bool operator!=(const Database& other) const
        {
            return !(*this == other);
        }

    private:
        std::string file;
        std::string name;
    };
}
\$\endgroup\$
5
  • \$\begingroup\$ You wrote your own stdafx? Why? \$\endgroup\$
    – Mast
    Feb 21, 2017 at 6:55
  • \$\begingroup\$ @Mast No, I modified the one VS provided \$\endgroup\$
    – skiwi
    Feb 21, 2017 at 8:10
  • \$\begingroup\$ I can't imagine modifying standard headers while keeping their original name is a good idea. \$\endgroup\$
    – Mast
    Feb 21, 2017 at 9:20
  • 1
    \$\begingroup\$ @Mast That's exactly what stdafx.h is used for. \$\endgroup\$
    – skiwi
    Feb 21, 2017 at 12:05
  • 3
    \$\begingroup\$ I'm downvoting this question because you haven't given any pure description about how your code works. No class summary, no nothing! \$\endgroup\$ Mar 4, 2017 at 17:02

1 Answer 1

2
\$\begingroup\$

Your code appears technically functional, but doesn't actually appear be very useful in storing any data.

  • It appears to have a suitable database file identification system
  • It appears to have a serialisation layer
  • It does not appear to have any kind of query layer stub
  • It does not appear to have a file layout layer stub
  • It does not appear to have file consistency / rebuilding mechanism stub

Note that depending on what your objective is then you should consider the following:

  • Relational DB => very, very difficult
  • Variable length data => need to build indexes
  • Sql or other text input => need a language parser, query constructor and evaluator
  • In place database file modification (avoiding entire read in and write out of the database) => need to be able to have intertwined, extensible and space managed (to handle any deletions and insertions) data structures.

You absolutely can greatly increase performance / reduce server cost if you happen to have a data flow that can be optimised for; but at the cost of being locked into an inflexible, unproven data storage mechanism that you have to make and maintain yourself. I have made the following (propriety code) which had much higher performance than the general purpose database it replaced.

  • append to /selection from tables only
  • single pass table generation
  • single file per table or index
  • variable sized data using an index
  • index re-generation though self describing parsable tables
  • tables contain aligned (with file system efficient storage block multiple) framed data with CRC checks for corruption recovery
  • chunked indexes with CRC for corruption recovery
  • eventual commitment to disk (in memory write back single point of truth cache) with extent locking for concurrent usage with block writes for throughput performance
  • all appends on a table are transactional and sequenced
  • queries are asynchronous and not blocked by each other or appends)
  • tcp binary database server implementation with non-text class based API
  • live snapshotting for incremental backups with throttling disk bandwidth
  • written in C#
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.