Implementation of a Caesar cipher is a popular exercise and there are many implementations posted here on Code Review.
My version is intended to be efficient and portable (subject to some limitations, below).
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <numeric>
#include <stdexcept>
class caesar_rotator {
using char_table = std::array<char, UCHAR_MAX+1>;
const char_table table;
public:
caesar_rotator(int rotation) noexcept
: table{create_table(rotation)}
{}
char operator()(char c) const noexcept
{
return table[static_cast<unsigned char>(c)];
};
private:
#define LETTERS "abcdefghijklmnopqrstuvwxyz"
static char_table create_table(int rotation)
{
constexpr int len = (sizeof LETTERS) - 1; // don't count the terminating null
static const auto* alpha2 = reinterpret_cast<const unsigned char*>(LETTERS LETTERS);
// normalise to the smallest positive equivalent
rotation = (rotation % len + len) % len;
char_table table;
// begin with a identity mapping
std::iota(table.begin(), table.end(), 0);
// change the mapping of letters
for (auto i = 0; i < len; ++i) {
table[alpha2[i]] = alpha2[i+rotation];
table[std::toupper(alpha2[i])] = static_cast<char>(std::toupper(alpha2[i+rotation]));
}
return table;
}
#undef LETTERS
};
int main(int argc, char **argv)
{
constexpr int default_rotation = 13;
// Parse arguments
int rotation;
if (argc <= 1) {
rotation = default_rotation;
} else if (argc == 2) {
try {
std::size_t end;
rotation = std::stoi(argv[1], &end);
if (argv[1][end]) { throw std::invalid_argument(""); }
} catch (...) {
std::cerr << "Invalid Caesar shift value: " << argv[1] << " (integer required)\n";
return EXIT_FAILURE;
}
} else {
std::cerr << "Usage: " << argv[0]
<< " [NUMBER]\nCaesar-shift letters in standard input by NUMBER places (default "
<< default_rotation <<")\n";
return EXIT_FAILURE;
}
// Now filter the input
std::transform(std::istreambuf_iterator<char>{std::cin},
std::istreambuf_iterator<char>{},
std::ostreambuf_iterator<char>{std::cout},
caesar_rotator{rotation});
}
Features and limitations
- It's written using portable C++ which compiles with any compiler supporting C++11 or later. (But don't let that inhibit reviews from suggesting improvements that require newer versions - I am still interested!)
- We can shift by any representable integer (negative shift decodes a positive shift, and vice versa).
- It works with all single-byte character sets. Unlike many implementations, it works on systems where alphabetic characters are discontinuous, such as EBCDIC.
- It works with multi-byte character sets where the alphabetic characters are invariant (e.g. UTF-8). When using codings that shift out the alpha-chars, then non-alphabetic characters may be modified - but using the same program for the decoding will correctly restore the original input text.
- Because we use a table to produce output, we need storage for that table, sufficient to represent all character values. No problem for systems where
CHAR_BIT
is 8, but it may prevent use on systems with much wider character type. - There is an overhead to constructing the table each run, which won't pay for itself with short input streams. But it's still small compared with the overhead of creating a process, and it's a big win for large inputs.
- I may write a separate wide-char implementation that addresses the limitations.
The ugly stuff
- I don't have access to an EBCDIC system to confirm my claim that it's correct on such platforms.
- Most of the ugliness is in
create_table()
, which in an earlier version had been an immediately-invoked lambda expression in the constructor's initialiser list; I moved it out to a named private function to make it easier to read and easier to skip when perusing the public interface. - I don't like using a macro to concatenate two copies of the alphabet. But I couldn't find an alternative
constexpr
way to do that, even in C++20, other than writing the letters twice, which I didn't want to do. (Andalpha2
still isn'tconstexpr
, because of the cast, but at least it enables reasonable compilers to store the string literal in.text
segment or equivalent.) - The
reinterpret_cast
offends me. And thestatic_cast
s, though to a lesser extent. But we need unsigned values forstd::toupper()
and for array indexing, and plainchar
for the stream handling and string literal. - I don't much like using
%
twice to get the positive modulo; is there a simpler way? I consideredrotation %= len; if (rotation < 0) rotation += len;
but I'm not convinced that's better. - In the main function, I throw an exception when argument has anything following an initial number, so that I handle all parsing errors in one place. Is that okay, or is there something better I could do?