Вопрос: Как написать простой калькулятор?

Лучший ответ: Я отвечу для случая работы с простым форматом ввода:
12345 + 6789

1) вы не модифицируете входную строку, поэтому, если имеем дело со стандартом от c++17, то лучше в функцию принимать явно string_view или const string_view&, если ниже, то принимать надо const string&. 

2) если уж принимаем что-то строкоподобное, то смело пользуемся функцией-членом ::find, которая есть и в строке и в вью. С её помощью можно найти сразу позицию арифметического символа. Для простого случая, даже не нужен обход от найденной позиции к началу строки и к концу строки для проверок std::isspace, является ли символ пробелом, поскольку мы можем сразу слать в atoi для левого числа например string_view::substr(0, opPos - 1). И, получится, только один if/switch, который будет сопоставлять символ операции, например, с указателем на функцию.  
...
int plus(int left, int right) { return left + right; }

using CalculatorFunction = int (*)(int, int);
...
CalculatorFunction operation;
...
switch (opChar):
case '+': operation = plus;
...

3) настоятельно рекомендую после этого реализовать нормальный простой калькулятор, а потом третьим заходом добавить поддержку скобок, hex oct бинарную и с мантиссой нотации записи чисел, операцию степени и корня. 
Нормальный - имеется ввиду, что в калькулятор входит строка, которая является корректным арифметическим выражением.  В этом случае нужно ещё иметь дело с разделителями. По-хорошему,  решение состоит из нескольких этапов: 

 - на первом логическом этапе функция парсит выражение и сохраняет в контексте или возвращает контейнер с токенами, (или прерывает весь процесс из-за плохой строки). В вашем варианте, задача однопоточная, поэтому можно воспользоваться функцией strtok, чтобы удобно извлечь строковое значение токена по входной строке от разделителя до разделителя.

 - как вариант, токены могут быть структурами с членом string_view на строковые представления и членом на тип токена (число, операция).

 - на втором логическом этапе мы считываем последовательность токенов и делаем какое-то действие:

считали число - ожидаем за ним токен операции 
если оказался не токен операции - бросаем исключение о некорректном арифметическом выражении, иначе - подготавливаем аргументы и  кормим функцию evaluate<T>( Token::kOperation op, T arg1, T arg2) , для случая, если операции нужно два аргумента, то первый уже считан, а второй будет в последовательности токенов за операцией. Результат evalaute записываем в переменную результата, которая, кстати говоря, типа double. 
продолжаем движение по последовательности токенов до конца, контейнером может быть std::list, когда добрались до конца, возвращаем результат. 


Худший ответ: Например:
atoi(ex.c_str())+atoi(ex.c_str())
это выражение 2 раза преобразует в число одну и ту же строку, а затем складывает эти 2 одинаковых числа.
Так что "не дублирует", а выполняет то что написано у вас в коде.

Вам нужно сначала разделить строку на "токены", т.е. на операнды и операции.
Если вы на этом этапе расчитываете обрабатывать только простейшие действия, то вводите строку и разбиваете ее на 3 токена. Затем операнды преобразуете в числа и после этого уже выполняете действие.