4
\$\begingroup\$

I'm working on learning C++ coming from python. I'm doing some projects to help me out understanding the language a bit more.

My code works I just want to know if there is anything that I can improve on it or best practices in C++, maybe something that I should have done differently.

// Ceaser Cipher implementation

#include <iostream>
#include <string>




char shiftF(char ch, int shift, const char type)
{
    if (type == 'e')
    {
        return ch == 'z' || ch == 'Z' ? ch - 25 : ch + shift;
    }
    else if (type == 'd')
    {
        return ch == 'z' || ch == 'Z' ? ch - 25 : ch - shift;
    }
}


std::string encode(std::string str, int shift, char type)
{
    std::string tempMsg;
    for (auto ch : str)
    {
        if (isalpha(ch))
        {
            tempMsg += shiftF(ch, shift, type);
        }
        else if (isspace(ch))
        {
            // There is probably a better way to do this.
            tempMsg += " "; 
        }
        else if (isalnum(ch))
        {
            continue;
        }
    }
    return tempMsg;
}


int main(int argc, char *argv[])
{
    int choice;
    std::cout << "What do you want to do? 1.Encrypt, 2.Decrypt: ";
    std::cin >> choice;
    std::string result;
    const char dec = 'd';
    const char enc = 'e';

    if (choice == 1)
    {
        int key;
        std::cout << "Enter encryption shift: ";
        std::cin >> key;

        std::string msg;
        std::cout << "Enter a message: ";

        // This doesn't work for some reason?
        // std::cin.clear();
        // std::cin.sync();

        std::cin.ignore();
        std::getline(std::cin, msg);

        result = encode(msg, key, enc);
    }

    else if (choice == 2)
    {
        int key;
        std::cout << "Enter decryption shift: ";
        std::cin >> key;

        std::string msg;
        std::cout << "Enter a message: ";

        std::cin.ignore();
        std::getline(std::cin, msg);

        result = encode(msg, key, dec);
    }
    else
    {
        std::cout << "Wrong option, exiting!";
    }

    std::cout << "Message encoded: " << result;
}
\$\endgroup\$
1
  • \$\begingroup\$ Given your follow-up question, I would encourage you to accept on the of the answers below (click the tick mark below the upvote/downvote buttons). This helps the StackExchange system by treating your question as "solved" instead of bumping it to front page periodically. It also avoids not skewing site statistics. \$\endgroup\$
    – Juho
    Commented Mar 10, 2019 at 12:08

3 Answers 3

3
\$\begingroup\$

Your functions are doing too much. Remember: one function, one responsibility. That is, the encode function actually both encodes and decodes and that behavior is controlled via its arguments. In particular, the choice of behavior is offloaded to shiftF based on a char. There is no error if that char is anything other than 'd' or 'e'. This is something that we should absolutely try to avoid: we want to catch as many problems as we can at compile-time, but even runtime errors are preferred to silent errors!

On closer inspection, there is no need for such a complication. In fact, the only difference between encoding and decoding is the sign of shift. So we could write:

std::string encode(const std::string& str, int shift)
{
    std::string tempMsg;

    std::transform(str.cbegin(), str.cend(), std::back_inserter(tempMsg), [&](char ch) -> char
    { 
        if (isspace(ch))
            return ' ';

        return ch == 'z' || ch == 'Z' ? ch - 25 : ch + shift; 
    });

    return tempMsg;
}

std::string decode(const std::string& str, int shift)
{
    return encode(str, -1 * shift);
}

What is happening here?

  • We pass the input message str as a const-ref and not by-value as in your original code. When you pass by-value, the object gets copied and in this case there is no reason for it. In this case, you could actually even pass the object by-reference only, and modify it in-place but we're not doing it here.

  • We use a standard function std::transform with a lambda function that encapsulates the logic of shiftF. You need to include <algorithm> for this use.

  • The third argument for std::transform is std::back_inserter found from <iterator>, which takes care of inserting at the back of the string tempMsg.

  • Conceptually, the division of encoding and decoding into separate functions is cleaner and more logical. Whenever you can implement another function in terms of other functions, it's likely a good idea because you don't have to repeat yourself leading to less maintenance decreasing the chances for bugs.

Once you adopt this approach, you can get rid of the const char variables from your main program, and just call the correct encode or decode function inside your if-statement. As a side remark, you could also return meaningful error codes in your main. For example, if the choice is invalid, return from the else-branch e.g., EXIT_FAILURE.

\$\endgroup\$
5
  • \$\begingroup\$ Does std::transform act as the for loop? Another question, why are you using the reference for str? I still get confused when to use references and pointers. \$\endgroup\$
    – Exzlanttt
    Commented Mar 7, 2019 at 15:10
  • \$\begingroup\$ (1) Yes, with std::transform the first two parameters say "go from begin to end in this range", the third argument says "this is where the result from each iteration of the loop goes to" and the fourth argument is "apply this operation in the loop". (2) I use a constant reference to str. When it's const, you can't modify its content inside the function. This is good because it conveys meaning and gives you protection from unintended mistakes. The reference means you do not copy the argument into the function, but directly play with that argument. Does it make sense? \$\endgroup\$
    – Juho
    Commented Mar 7, 2019 at 15:16
  • \$\begingroup\$ And basically, just don't use pointers unless you have to. They are not evil, but you have to be careful especially if you use "naked pointers" (e.g., allocate memory dynamically without so-called smart pointers). If you talk about argument passing (that's a keyword you can search for), cheap-to-copy built-ins (int, float, char) you usually pass as int x and so. With more expensive objects, you should avoid copying them (like std::string etc.) and pass those by reference (which you should make const as well unless you need to modify the object). \$\endgroup\$
    – Juho
    Commented Mar 7, 2019 at 15:20
  • \$\begingroup\$ You explained it very well thank you! So if I use references I'm passing the object itself otherwise it's a copy? Why should I use the object itself? Memory efficiency? Coming from python/java pointers and references kill me. \$\endgroup\$
    – Exzlanttt
    Commented Mar 7, 2019 at 15:30
  • \$\begingroup\$ Exactly, that's basically it. And yep, why copy a string if you want to say convert it to uppercase, taking it by reference avoids unnecessary copying. Having these options roughly means you have more control over your program, which unlocks potential for more performance but also makes it easier to shoot yourself in the foot. \$\endgroup\$
    – Juho
    Commented Mar 7, 2019 at 15:56
3
\$\begingroup\$

The code makes the assumption that the runtime character set encodes letters contiguously in order. Whilst this may be true on your system, it will fail on platforms that use EBCDIC or other codings with gaps between letters. It will also fail for codings such as IS 8859, which have letters outside of a..z,A..Z (e.g. à).

We're missing the include of <cctype> which defines std::isalpha, std::isspace and std::isalnum (all of which are misspelt in the code).

When reading input, always have a plan for what happens if the read fails. So std::cin >> key needs to be if (std::cin >> key) with appropriate code in the if and else branches.

Consider working as a filter for standard input, rather than operating on just a single line.

\$\endgroup\$
2
  • \$\begingroup\$ I'm using visual studio and it didn't show any errors on isalpha/isspace, strange and when I ran "Hello World" it handled the space well and entered the isspace condition. \$\endgroup\$
    – Exzlanttt
    Commented Mar 7, 2019 at 12:21
  • 1
    \$\begingroup\$ It's unfortunate that even the best compilers can't spot all the possible portability problems in our code - it still takes a human to pick up some of these. \$\endgroup\$ Commented Mar 7, 2019 at 13:04
1
\$\begingroup\$

Not sure you ceaser cipher is working as expected:

return ch == 'z' || ch == 'Z' ? ch - 25 : ch + shift;

There is a special case for z or Z. This is not symmetric so decoding an encoded 'Z' or 'z' will fail. Also letters can be encoded as punctuation (which is why your encode() does not even try to enocde punctuation. I would fix this so that letters are encoded as letters (everything else is unencoded).

I would change your encoding to:

// int min:   Pass 'a' for std::islower() and 'A' for std::isupper()
// int dir:   Pass 1 for encode and -1 for decode.
char code(char ch, int shift, char dir, int min)
{
    return (ch + min + (dir * shift) % 26) + min;
}

Now that letters are encoced into other letters only (and don't splash out in into the punctuation range). We can simplify the shift function. It will handle letters and all other characters are left unencoded.

char shiftF(char ch, int shift, char dir)
{
    if (std::isalpha(ch) && std::islower(ch)) {
        return code(ch, shift, dir, 'a');
    }
    else if (std::isalpha(ch) && std::isupper(ch)) {
        return code(ch, shift, dir, 'A');
    }
    return ch;
}

As the shift now handles all letters correctly we don't need to any real work in the encode. But to make it safer and stop a sensitive string from being leaked around memory lets us change it to update the string in place.

void encode(std::string& str, int shift, char dir)
{
    for (auto& ch : str) {
        ch = shiftF(ch, shift, dir);
    }
}
\$\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.