C++17 In Detail

18 May 2020

Spaceship Generator for May the 4th in C++ - Results!

Spaceship Random Generator, C++

Two weeks ago, I announced a little game on my blog! Today I’d like to present some of the solutions you sent me and discuss a few things from various aspects of Modern C++.

Big Thanks!

First of all, I’d like to thank all of the participants for sending the solutions. I got 14 of them.

Although the task might sound easy, it required between 100 or 300 lines of code. So it wasn’t just a five-minute coding session. Thanks for your time and I hope it was a funny experience for you :)

Rules Reminder

The task for the game was as follows: write a random spaceship generator which can create amazing spaceships (*)!

(*) Not to mistake with the spaceship operator for C++20 :)

For example:

Spaceship with ionizing engine, small wings, regular cabin, tie fighter style fuselage and rocket launcher weapon.

Prizes

Each participant got a chance to win the following reward:

3-month Educative.io service and 15$ Amazon.com Gift Card

I have 5 of those “packs” for five people.

The winners were selected randomly from all of the participants and should already get notifications.

The Starting Legacy Code

Please have a look at my initial example :)

#include <string> 
#include <cstring> 
#include <iostream>
#include <vector>  
#include <fstream>
#include <random>
#include <algorithm>

char partsFileName[128] = "vehicle_parts.txt";

std::vector<std::string> allParts;

class Spaceship {
public:
    static void GenerateShip(SpaceShip *pOutShip);

    void Print() {
        // print code...
    }
private:
    std::string _engine;
    std::string _fuselage;
    std::string _cabin;
    std::string _large_wings; // optional
    std::string _small_wings;  // optional
    std::string _armor;
    std::string _weapons[4]; // max weapon count is 4
};

void Spaceship::GenerateShip(Spaceship *pOutShip) {
    std::vector<std::string> engineParts;
    std::vector<std::string> fuselageParts;
    std::vector<std::string> cabinParts;
    std::vector<std::string> wingsParts;
    std::vector<std::string> armorParts;
    std::vector<std::string> weaponParts;

    for (const auto& str : allParts) {
        if (str.rfind("engine") != std::string::npos)
            engineParts.push_back(str);
        else if (str.rfind("fuselage") != std::string::npos)
            fuselageParts.push_back(str);
        else if (str.rfind("cabin") != std::string::npos)
            cabinParts.push_back(str);
        else if (str.rfind("wings") != std::string::npos)
            wingsParts.push_back(str);
        else if (str.rfind("armor") != std::string::npos)
            armorParts.push_back(str);
        else if (str.rfind("weapon") != std::string::npos)
            weaponParts.push_back(str);
    }

    std::random_device rd;
    std::mt19937 g(rd());

    std::shuffle(engineParts.begin(), engineParts.end(), g);
    std::shuffle(fuselageParts.begin(), fuselageParts.end(), g);
    std::shuffle(cabinParts.begin(), cabinParts.end(), g);
    std::shuffle(wingsParts.begin(), wingsParts.end(), g);
    std::shuffle(armorParts.begin(), armorParts.end(), g);
    std::shuffle(weaponParts.begin(), weaponParts.end(), g);

    // select parts:
    pOutShip->_engine = engineParts[0];
    pOutShip->_fuselage = fuselageParts[0];
    pOutShip->_cabin = cabinParts[0];
    pOutShip->_armor = armorParts[0];
    pOutShip->_large_wings = wingsParts[0];
    pOutShip->_weapons[0] = weaponParts[0];
}

int main(int argc, char* argv[]) {
    if (argc > 1) {
        strcpy(partsFileName, argv[1]);
    }    

    std::cout << "parts loaded from: " << partsFileName << '\n';

    std::ifstream file(partsFileName);
    if (file.is_open()) {
        std::string line;
        while (std::getline(file, line)) {
            allParts.push_back(line);
        }
        file.close();
    }     

    Spaceship sp;
    Spaceship::GenerateShip(&sp);
    sp.Print();
}

As you can see above the program consists of several parts:

  • It reads all the lines from a given file and stores it in a global vector of strings. Yes… global, as it’s the best way to program such programs :)
  • Of course almost no error checking needed :)
  • Then we define a Spaceship with the best possible name sp.
  • Later the spaceship is passed to a generator function that does the main job:
    • It sorts the input parts and groups them into separate containers.
    • Then it shuffles the parts containers
    • We can then use the first objects in those containers and assign them to the appropriate member variables of the output spaceship
  • At the end, the main function invokes a print member function that shows the generated spaceship.

Can you write better code? :)

Yes, you can! Through your submissions, you managed to fix all of my bad patterns :)

Some Cool Ideas

Here are the code samples extracted from submissions.

Getting rid of global variables

First of all, my super-advanced starting code example used global variables. The submitted code nicely fixed this issue by using only local variables.

For example in solution from Thomas H. there’s a separate class that contains all parts, this is a small Database:

PartDB partDB = readPartDB(partsFileName);
const Spaceship sp = makeRandomSpaceShip(partDB);

And the details:

struct PartDB {
    std::vector<Engine> engines;
    std::vector<Fuselage> fuselages;
    std::vector<Cabin> cabins;
    std::vector<Armor> armors;
    std::vector<Wing> wings;
    std::vector<Weapon> weapons;
    std::vector<Shield> shields;
};

PartDB readPartDB(const std::filesystem::path& partsFileName) {
    PartDB partDB;

    std::ifstream file(partsFileName);
    if (file.is_open()) {
        std::string line;
        while (std::getline(file, line)) {
            if (line.rfind("engine") != std::string::npos) {
                partDB.engines.push_back(Engine{line});
            } else if (line.rfind("fuselage") != std::string::npos) {
                // ...
            } else {
                std::cerr << "Unknown part: '" << line << " '\n";
            }
        }
    }

    return partDB;
}

This is nice and a simple way to keep all the parts in one place. My starting code mixed loading with the generation, so it was not the best pattern.

Clever way of loading data

In my starting code I used only a vector of strings to store all the parts. But many solutions improved that by using maps and even maps of variants:

void GetDataFromFile()
    {
        std::string line;
        inputData.exceptions(std::ifstream::badbit);
        while (std::getline(inputData, line))
        {
            int n = line.rfind(" ");
            std::array<std::string, 2> arStrParts{ line.substr(0, n), line.substr(n + 1) };
            if (auto it = umShipParts.find(arStrParts[1]); it != umShipParts.end())
            {
                std::visit([&arStrParts](auto& obj) { obj.add(arStrParts[0]); }, umShipParts[arStrParts[1]]);
            }
        }
    }

More in the full solution from Mike @Wandbox

Another cool example we can find in the code created by Marius Bancila:

part_type find_part_type(std::string_view description)
{
   static std::vector<part_description> parts
   {
      {part_type::engine,  {"engine"}},
      {part_type::fuselage,{"fuselage"}},
      {part_type::cabin,   {"cabin"}},
      {part_type::wings,   {"wings"}},
      {part_type::armor,   {"armor", "shield"}},
      {part_type::weapon,  {"weapon"}}
   };

   for (auto const & [type, desc] : parts)
   {
      for (auto const& d : desc)
      {
         if (description.rfind(d) != std::string::npos)
            return type;
      }
   }

   throw std::runtime_error("unknown part");
}

In the above examples, you can see that we have much better code, more readable and scalable (if you want to add new types of parts).

Getting more flexibility

In the another solution Michal stored also the names of the parts:

for (auto&& partsLine : partLines)
 {
    auto key   = utils::last_word(partsLine);
    auto part  = partsLine.substr(0, partsLine.size() - key.size() - 1);
    auto keyIt = parts.find(key);

    if (keyIt == parts.end())
    {
        parts.try_emplace(std::move(key), std::vector<std::string> {std::move(part)});
    }
    else
    {
        parts.at(key).emplace_back(std::move(part));
    }
 }

This approach allows to specify the mandatory parts in a just an array, without creating the types for each part:

constexpr auto mandatoryParts = {"engine"sv, "fuselage"sv, "cabin"sv, "armor"sv};

Have a look @Wandbox

Getting the full flexibility

Also, I’d like to draw your attention to the example sent by JFT who went even further with the flexibility. Rather than fixing the specification of the spaceship in code, he described it in the parts file.

That way, the design of the spaceship is fully customisable, and there’s no need to change code of the application. What’s more, the author managed to write quite concise code, so it’s quite short:

Example of a spaceship design:

1 engine
1 fuselage
1 cabin
1 armor
-4 weapon
-1 wings_s
-1 wings_l
-1 shield

where:

where    number_required is:
            0 to ignore
            > 0 for required up to
            < 0 for optional up to

The code is available here @Wandbox

Pain with enums

In a few examples I’ve noticed the following code:

enum class spaceshipPartsEnum
{
    engine,
    fuselage,
    cabin,
    wings,
    armor,
    weapon
};

And then the tostring() method.

std::string enum_to_string (spaceshipPartsEnum part)
{
    switch (part)
    {
        case spaceshipPartsEnum::engine:
            return "engine";
        case spaceshipPartsEnum::fuselage:
            return "fuselage";
        case spaceshipPartsEnum::cabin:
            return "cabin";
        case spaceshipPartsEnum::wings:
            return "wings";
        case spaceshipPartsEnum::armor:
            return "armor";
        case spaceshipPartsEnum::weapon:
            return "weapon"; 
    }

    assert (false);
    return {};
}

It would be great to have native support for enum to string conversions!

Useful utils

From Michal: See @Wandbox


namespace utils
{
    /**
     *  Just a siple wrapper of random nuber generator.
     */
    class random_uniform_int
    {
    private:
        std::mt19937 generator_;
        std::uniform_int_distribution<size_t> distribution_;

    public:
        random_uniform_int(size_t const min, size_t const max, unsigned long const seed) :
            generator_    {seed},
            distribution_ {min, max} 
        {
        }

        auto next_index () -> size_t
        {
            return distribution_(generator_);
        }
    };

    /**
     *  Just a siple wrapper of random nuber generator.
     */
    class random_bool
    {
    private:
        std::mt19937 generator_;
        std::uniform_real_distribution<double> distribution_;

    public:
        random_bool(unsigned long const seed) :
            generator_    {seed},
            distribution_ {0, 1}
        {
        }

        auto next_bool () -> bool
        {
            return distribution_(generator_) < 0.5;
        }
    };

    auto last_word (const std::string& s) -> std::string
    {
        auto const lastSpaceIndex = s.rfind(' ');

        if (lastSpaceIndex == std::string::npos)
        {
            return "";
        }

        return s.substr(lastSpaceIndex + 1);
    }
}

C++20 Parts

I guess that one of the easiest features that you could use from C++20 is starts_with or ends_with member functions that we get for string and string views: In the example from Benjamin he used it to replace rfind() calls:

Have a look @Wandbox

Warehouse& Warehouse::add(std::string description) {
    if (description.ends_with("engine")) {
        engines_.emplace_back(std::move(description));
    } else if (description.ends_with("fuselage")) {
        fuselage_.emplace_back(std::move(description));       
        // ...

And if you’d like to see more of C++, have a look at this code from Jackson @Wandbox. He used ranges and concepts and also…

And also one of the coolest use of the spaceship operator:

// Spaceship for the Spaceship :)
auto operator<=>(const Spaceship& other) const noexcept = default;

Summary

One again thank you for all the solutions! In this short blog post, I managed to extract only a few bits of code, but there’s more to that.
Your solutions even got part validation, logging, template machinery and much more cool ideas!

If you want to get additional C++ resources, exlusive articles, early access content, private Discord server and weekly curated news, check out my Patreon website: (see all benefits):

© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.