Welcome to the fifth and final episode of Learning C++ with C++Builder Starter. Last time, we had a calculator and numerical input, demonstrating using boost::optional, smart pointers, and more. This post is shorter: it will finish the calculator with operators (implementing addition, subtraction, etc) and end with a full application, ready to move to cross-platform if you wish.
Operators
The calculator is almost functional. The one thing missing is actually performing the math operations.
We want to support four operations: addition, subtraction, multiplication, and division. Each of these is an operation; each takes two operands, a left and a right operand. In the expression
3 x 4
The left operand is 3, the right operand is 4, and the operation is Multiply.
This gives us a good basis for an interface, and an object-oriented design for operations, and is a good excuse to make a polymorphic class hierarchy with virtual methods – a useful thing to see done when learning C++. Let’s make an interface and factory methods in a new unit called uOperator.cpp:
. class IOperator { public: virtual double Calc(const double A,const double B) = 0; virtual std::wstring Name() const = 0; }; std::unique_ptr<IOperator> CreateOp(const EOperator Op);
And implement these in the .cpp file. For example, Add would be:
. class TAdd : public IOperator { virtual double Calc(const double A, const double B) { return A + B; } virtual std::wstring Name() const { return L"+"; } };
Along with the factory method, switching on Op.
Any call to the interface's methods, Calc or Name, will call into the implemented descendant.
Next, create this when the user sets the operator. Add a std::unique_ptr<IOperator> m_Op; to the TCalculator class, and change TCalculator::SetOperator to look like so:
. if (Op == EOperator::eNull) { m_Op.reset(); } else { m_Op = CreateOp(Op); } UpdateUI(); }
This is currently simplified – we’ll examine some other things that have to happen here in a moment.
This allows us to calculate the result of an operator, so long as we have two numbers. How do we get those two numbers?
Left and Right Operands, and the Result
Last post, we had an accumulator – a small object whose job was to take button keypresses and turn them into numbers. When has the user finished typing?
- When the user presses an operator (like + or -)
- When the user presses Equals
In both these cases, it indicates one number is complete, and something else needs to happen: in the first case, that the second number needs to be entered; in the second, to calculate the result. Also in the first case, entering the second number might mean calculating as well – if the user has entered ‘1 + 2’, and presses the multiply button, they want to multiply 3 by something. That something is the new right operand, and the result is the new left operand.
This means that SetOperator needs to be able to promote the accumulator’s value to a new left or right operand value, and also sometimes invoke Equals and moving the result to the left operand, before setting the operator.
Secondly, the left and right operands, and the result, do not always exist. When the calculator app has just started, and the user has entered nothing, there are no operands or result. When Equals has been pressed, there is a result but no left or right operand. This harks back to last week’s post: how do you represent something that may or may not exist? Using boost::optional.
Let’s add three optional values to the calculator class’s declaration:
. boost::optional<double> m_Left; boost::optional<double> m_Right; boost::optional<double> m_Result;
They’re not initialised to any value in the class constructor, since at app startup they are empty.
Promoting the accumulator
First, let’s create a method for moving a number from being typed in (the accumulator) to a left or right operand. It’s fairly simple: if there is no left operand, initialize the left with the accumulator’s value; if there is, then initialize the right operand with the accumulator’s value. If there’s already both a left and a right, that indicates a logic error.
. void TCalculator::PromoteAccumulator() { if (!m_Accum.Entering()) { return; } if (!m_Left) { m_Left = m_Accum.Value(); } else { assert(!m_Right); m_Right = m_Accum.Value(); } // Reset m_Accum = Accumulator(); }
Setting the operator
We can now expand the method called when the user wants to set the operator – when they press + or -.
- If the operator changes, and the user was typing, that needs to be promoted to an operand.
- If there are two operands, both left and right, then it needs to calculate (this addresses when the user has typed 1, +, 2 and then presses *. In this situation they want to get the result of three, and multiply that by something soon to be typed in, so move the result to the left.)
- Assuming all this resulted in a left-side operand, create an operator class instance.
The code to do this looks like so:
. void TCalculator::SetOperator(const EOperator Op) { if (Op == EOperator::eNull) { m_Op.reset(); } else { PromoteAccumulator(); // once the op changes, stop entering, make it a number // 1 + 2 [now press -] -> want to calc that, make it the left operand if (!m_Result && m_Left && m_Right) { Equals(); } if (m_Result) { // Operator after result -> want the result to be the new left operand m_Left = m_Result; m_Right.reset(); m_Result.reset(); } if (m_Left) { // Require a left operand, because all ops have to operate on at least // one operand m_Op = CreateOp(Op); } } UpdateUI(); }
Calculating
Not much left! We can now make use of the operator classes we created at the beginning of the post. The calculation method, called Equals after the button, will promote the accumulator if necessary (the user might have been typing what should become a right-side operand); if it has a left, a right, and an operator, call the virtual Calc method to get a result.
. void TCalculator::Equals() { PromoteAccumulator(); if (m_Left && m_Right && m_Op) { m_Result = m_Op->Calc(*m_Left, *m_Right); m_Left.reset(); // No operands any more m_Right.reset(); SetOperator(EOperator::eNull); // Nothing to do any more } UpdateUI(); }
Printing
One final step: displaying this onscreen.
A traditional calculator only displays the number currently being entered, or the result. The operator interface we designed has an operator name, and there’s no reason our app can’t be slightly more advanced than a desktop calculator. If the user is trying to calculate “1 + 2” why display only 1, or only 2, and no sight of ‘+’?
My version of this code will show the left and right operands, if they exist, along with the operator if it’s set, accounting for the user typing (for example, there might not be a right operand, just the accumulator in use – same with the left operand.)
There's not much new here: this uses string streams, and dereferences the optional left or right values.
. void TCalculator::UpdateUI() { std::wstringstream stream; if (m_Left) { stream << *m_Left; if (m_Op) { stream << L" " << m_Op->Name() << L" "; if (m_Right) { stream << *m_Right; } else if (m_Accum.Entering()) { stream << m_Accum.ValueStr(); } } } else if (m_Accum.Entering()) { stream << m_Accum.ValueStr(); } else if (m_Result) { stream << *m_Result; } m_Display.UpdateUI(stream.str()); }
The Calculator
And that’s it. Really. Congratulations, you now have a fully functional calculator!
This code, being FireMonkey, can be compiled on Windows, macOS, and mobile. (Note if you want to target Android, you'll need to install Boost for Mobile.) You can theme it, and change the layout for any device using the multi-device designer. You've created a cross-platform app without even trying.
We've covered:
- Basics
- Installing C++Builder Starter
- Basic debugging
- Designing a UI
- UI design
- FMX styles, to give your app a new theme
- Coding
- Basic C++ syntax
- Basic object oriented classes in C++
- Useful C++ classes and techniques, including smart pointers and optionals, an understanding of heap-allocated objects, RAII, and other C++ basics
- Good application architecture: separating the UI and logic
- Interfaces and implementing interfaces
- Good application architecture: principles of ownership
and ended with a complete, working, very nice calculator. Your challenge? Recompile it for macOS or iOS! (Hint: investigate the 'Target Platforms' node in the Project Manager…)
Thanks for reading each post. I hope you learned some good stuff!
Source code
Read more
The Learn to program C++ with C++Builder series:
- Part 1: Introduction and Installation
- Part 2: Building and Debugging
- Part 3: Design, Architecture, and UIs
- Part 4: Real Code and Useful C++: Ownership, smart pointers, styles, and optional values
- Part 5: Operators, and Final Application!