Start learning free

LESSON 560 OF 568 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in C++: Abstract Classes and Pure Virtual Methods

Practice O(1) Read the full explanation

A shop takes payments by card, by PayPal and in cash. Every payment has an amount, in cents, and a total: the amount plus a processing fee. The total is worked out the same way for every payment; only the fee depends on the kind of payment. That is what an abstract class is for: it holds the shared part, says that every payment has a fee, and leaves the fee itself to each kind of payment.

  • The abstract class Payment is built from an amount, in cents, and keeps it. Its method fee() is abstract: every payment has a fee, but Payment can't say what it is. Its method total() is concrete: it returns the amount plus fee().
  • Card, PayPal and Cash inherit from Payment, keep its total() as it is, and each writes its own fee():
  • Card pays 2%: its fee is amount × 2 / 100.
  • PayPal pays 3% plus 30 cents: its fee is amount × 3 / 100 + 30.
  • Cash costs nothing: its fee is 0.

C++ has no abstract keyword: a pure virtual method, virtual int fee() = 0;, makes Payment abstract. Each subclass inherits with : public Payment, hands its amount to Payment's constructor with : Payment(amount), and overrides fee. Dividing two ints drops the fraction.

Every division rounds down: a card payment of 1999 cents has a fee of 39, not 39.98.

The starter code already has every class, every constructor and every method header: declare the field and the pure virtual method, and write the bodies. For each test, the harness creates the payments in the order given and prints two lines: each payment's total, total(), separated by spaces, and then the sum of all the fees. It calls them through the abstract type Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    new Card(1999)
    new PayPal(500)
    new Cash(700)
Output:
    2038 545 700
    84
Explanation: The card's fee is 1999 × 2 / 100 = 39 (39.98 rounded down), so its total is 2038. PayPal's fee is 500 × 3 / 100 + 30 = 45, total 545. Cash has no fee: total 700. The fees add up to 39 + 45 + 0 = 84.

Example 2:

Input:
    new Card(49)
    new PayPal(33)
    new Card(50)
Output:
    49 63 51
    31
Explanation: 49 × 2 / 100 is 0.98, rounded down to 0. PayPal's 33 × 3 / 100 is 0.99, rounded down to 0, plus 30. The second card's 50 × 2 / 100 is exactly 1. The fees are 0 + 30 + 1 = 31.

Example 3:

Input:
    new Card(0)
    new PayPal(0)
    new Cash(0)
Output:
    0 30 0
    30
Explanation: Amounts can be 0. PayPal still adds its 30 cents.

 

Constraints:

  • Every test has between 1 and 100 payments.
  • Every amount is an integer from 0 to 106 cents.

Note:

Every fee and every total takes O(1) time.

STEP 1 OF 7
    Ctrl + Enter to check

    C++ · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in C++, explained

    Problem overview

    A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total, which is the amount plus a processing fee. The total is worked out the same way for every payment. Only the fee depends on the kind of payment:

    • Card: amount * 2 / 100;
    • PayPal: amount * 3 / 100 + 30;
    • Cash: 0.

    All divisions are on ints, so they round down. A card payment of 1999 cents has a fee of 39, not 39.98, and its total is 2038. A PayPal payment of 500 cents has a fee of 500 * 3 / 100 + 30 = 45, so its total is 545. Cash of 700 stays 700. The three fees add up to 84.

    Approach: share the total, leave the fee open

    The rule for the total, amount plus fee, is the same everywhere, so it should be written once. The fee is different in every kind of payment, so the parent class can't write it. An abstract class handles exactly this: it holds the shared part, here the amount and total(), and declares that every payment has a fee() without saying what it is.

    C++ has no abstract keyword. Instead, a pure virtual method makes a class abstract:

    virtual int fee() = 0;
    

    The = 0 takes the place of a body. A class with at least one pure virtual method can't be created directly, and each child must override the method before it can be created.

    Because fee is virtual, the call inside total() runs the fee of the object's real class. A Card object's total() uses the card fee, with no extra code in Card.

    Without an abstract class you would have two poor options. You could copy total() into every kind of payment, and a later change to the rule would have to be made three times. Or you could give Payment a made-up default fee, and a new kind of payment that forgets to write its own would quietly get the wrong one. With a pure virtual method, the compiler refuses to create a payment that has no fee.

    Step by step

    1. In Payment, declare the field int amount; under public:. It lives in the parent once, and all three children inherit it.
    2. In the constructor, store the parameter with this->amount = amount;. An abstract class can have a constructor: it runs when a child is created.
    3. Declare fee as virtual int fee() = 0;.
    4. In total, return amount + fee().
    5. In Card, return amount * 2 / 100.
    6. In PayPal, return amount * 3 / 100 + 30.
    7. In Cash, return 0. It still has to write fee, because the parent left it abstract.

    The starter code already gives each child its constructor, Card(int amount) : Payment(amount) {}. After the colon, Payment(amount) passes the amount to the parent's constructor, which stores it.

    The solution in C++

    class Payment {
    public:
        int amount;
    
        Payment(int amount) {
            this->amount = amount;
        }
    
        virtual int fee() = 0;
    
        int total() {
            return amount + fee();
        }
    
        virtual ~Payment() = default;
    };
    
    class Card : public Payment {
    public:
        Card(int amount) : Payment(amount) {}
    
        int fee() override {
            return amount * 2 / 100;
        }
    };
    
    class PayPal : public Payment {
    public:
        PayPal(int amount) : Payment(amount) {}
    
        int fee() override {
            return amount * 3 / 100 + 30;
        }
    };
    
    class Cash : public Payment {
    public:
        Cash(int amount) : Payment(amount) {}
    
        int fee() override {
            return 0;
        }
    };
    

    A run

    Creating the payments of the first example through Payment* pointers and printing every total(), then the sum of the fee() values, gives:

    2038 545 700
    84
    

    For Card(49), PayPal(33) and Card(50) the output is:

    49 63 51
    31
    

    49 * 2 / 100 is 0.98, which rounds down to 0. The PayPal fee is 0 + 30. The second card is exactly 1. And for amounts of 0, Card(0), PayPal(0) and Cash(0) give 0 30 0 and a fee sum of 30: PayPal still charges its 30 cents.

    Details that matter

    • Multiply first, then divide. amount * 2 / 100 keeps the precision for as long as possible. For an amount of 150, it gives 3. Dividing first, amount / 100 * 2, gives 2.
    • No overflow here. The largest amount is 1,000,000, so amount * 3 is 3,000,000, far below the largest int.
    • total() isn't virtual and doesn't need to be: no child changes it. The children keep it as it is.
    • The virtual destructor virtual ~Payment() = default; lets delete on a Payment* free the real object correctly.

    Complexity

    Each fee and each total is a few arithmetic operations, so each call takes O(1) time and no extra space.

    Edge cases and common mistakes

    • Creating a plain Payment. Payment p(5); doesn't compile. The compiler says it cannot declare the variable to be of abstract type Payment, because the virtual function fee() is pure within it.
    • Leaving fee out of a child. If Cash has no fee, then Cash is still abstract, and Cash c(5); fails with the same kind of error: it cannot declare the variable to be of abstract type Cash. Write return 0; for it.
    • Forgetting PayPal's 30 cents when the amount is 0. The percentage is 0, but the 30 cents stay.
    • Dividing before multiplying, as shown above, rounds too early.
    • Writing a second total() in each child. The point of the parent is that the total is written once.

    Testing

    The three payments of each list are in order, with their totals and then the sum of the fees:

    PaymentsTotalsFees
    Card(1999), PayPal(500), Cash(700)2038 545 70084
    Card(49), PayPal(33), Card(50)49 63 5131
    Card(0), PayPal(0), Cash(0)0 30 030
    Keep going with C++500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free