I recently purchased and started reading a book entitled: The Modern C++ Challenge. This book contains interesting challenges that can be solved using modern C++ 17. Each challenge is unique and showcases a specific subset of the available facilities in the C++ language. The first challenge I chose to accept was modeling a simple IP address in C++.

What is an IP address?

Simply put, it’s a unique address to identify a particular “place” or “location” on the internet. The “IP” in IP address stands for Internet Protocol. Such an address is analogous to a street address which uniquely identifies a household.

As you can imagine, there are multiple protocols for IP addresses. The most common (and older) is IPv4 where an address is represented by 4 numbers separated by periods (.) as in 127.0.0.1. There is another protocol called IPv6 that uses “eight groups of four hexidecimal digits” and can look something like this (from wikipedia): 2001:0db8:85a3:0000:0000:8a2e:0370:7334.

Fortunately, in this challenge, the IP address protocol was specified, namely IPv4.

Challenge Requirements

The class had to meet the following requirements:

  1. Represent an IPv4 address.
  2. Read and write such an address to and from a string (i.e. console input).
  3. Format the printed version of the address to be of the form 127.0.0.1.
  4. Allow for address enumeration (for example print all addresses between 0.0.0.0 and 255.255.255.255).

Implementation

For my implementation I chose to name the class address and have it be defined inside a namespace ip so that the syntax to instantiate it could be something like this:

ip::address ip_address(127, 0, 0, 1);

This follows a practice that is present in many C++ projects such as boost. Additionally, I tried to follow the design principles that can be seen in standard template library with the definition of various typ aliases and functions so that the class can be used with the STL.

As shown in the snippet above, the class is instantiated with 4 numbers. These numbers cannot be arbitrary though; they must be positive integers in the range of 0-255. In C++ this is represented by an unsigned char but to be safe I used the type uint8_t defined in the stdint.h header. I also added other constructors for convience as can be seen below:

namespace ip
{
    /**
     * Class that models a IPv4 address. 
     */
    class address
    {
    public:
        address(value_type first, value_type second,
            value_type third, value_type fourth);
        address(const std::array<unsigned char, 4> &data);
        explicit address(uint32_t value);
        
        // ... more code
    }
}

The above code basically satisfies the first requirement. Next I decided to tackle the ability to convert the address to a string (for printing) and also read an address from a string. Instead of making this part of the class, I instead made it a “utility” function within the ip namespace.

ip::address ip::from_string(const std::string &view)
{
    auto parts = split(view, '.');
    if (parts.size() != 4)
    {
        throw invalid_format_exception(view);
    }

    return {
        (ip::address::value_type)std::stoi(parts[0]),
        (ip::address::value_type)std::stoi(parts[1]),
        (ip::address::value_type)std::stoi(parts[2]),
        (ip::address::value_type)std::stoi(parts[3])
    };
}

std::string ip::to_string(const address& address)
{
    std::ostringstream string_stream;
    string_stream << address;
    return string_stream.str();
}

Finally I added a stream operator overload so the class can easily be printed to the console.

std::ostream& ip::operator<<(std::ostream& output, const ip::address& address)
{
    std::copy(address.begin(), address.end()-1, 
        std::ostream_iterator<short>(output, "."));
    output << +address[3];
    return output;
}

These code snippets satisfy requirements 2 and 3.

Finally, to address the final requirement, I overloaded the increment and decrement operators. This makes it easy to enumerate addresses with a for loop but to do that I also needed comparison operators; so I did both. To make it even easier though, I added code to convert to and from a 32 bit integer. You can do some simple bit shifting with the 32 bit number to get 4 eight-bit integers from it. You can similarly convert 4 eight bit integers to a single 32 bit integer. With this code it becomes easy to compare two ip::address objects to each other like so:

uint32_t ip::to_uint32(const address& address)
{
	const uint32_t value = address[0] << 24 | address[1] << 16 | address[2] << 8 | address[3];
	return value;
}

bool ip::operator<(const ip::address& first, const ip::address& second)
{
	return to_uint32(first) < to_uint32(second);
}

bool ip::operator==(const ip::address& first, const ip::address& second)
{
    return to_uint32(first) == to_uint32(second);
}

ip::address& ip::address::operator++()
{
	auto value = to_uint32(*this);
	++value;                     
	(*this) = address(value);
	return *this;
}

::ip::address ip::address::operator++(int)
{
	const auto result(*this);
    ++(*this);
    return result;
}

ip::address& ip::address::operator--()
{
	auto value = to_uint32(*this); 
	--value;                     
	(*this) = address(value);
	return *this;
}

::ip::address ip::address::operator--(int)
{
	const auto result(*this);
    --(*this);
    return result;
}

And we’re done! Just like that we have a simple class that represents an IPv4 address in C++. I took the class a bit further to make it “nice” to use and also added comments in case anyone wants to use it in their code. As always the full implementation is available on Github. Feel free to leave questions, comments and suggestions below. If you enjoyed reading this post please consider following my blog or my YoutTube channel.

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...