6
\$\begingroup\$

I am modeling a programmable electrical power supply which has multiple independent channels (or "ports") in a class I'm calling Supply. One of its features is the ability to put pairs of ports in parallel in order to increase the power that can be supplied to a load. Conceptually, it looks similar to this:

Parallel power supplies

(Of course the real power supply is more complicated than a switch since you can't just short the outputs of two voltage sources.)

The number of ports \$N\$ in the power supply varies from instance to instance, but is always a constant for a given power supply instance and is always an even number. Two ports can be put in parallel with each other but not three or more. Consequently, the number of outputs (where "output" is defined as either a single port or a combined pair of ports) can vary from \$N/2\$ (when all ports are combined with another one) to \$N\$ (when no ports are in parallel with one another).

Since the number of outputs may vary over the lifetime of the power supply instance, there needs to be an easy way for the code that controls the power supply to iterate through all of the power supply's outputs as well as access the \$n\$th port or output (e.g. to set the desired output voltage). I've designed the Supply class to provide an iterator and an accessor to an object (called Output) which tracks the port number(s) assigned to a given output.

I'd like a review for the interface which allows iterating through the outputs and accessing a particular one. Note that the actual Supply class is more complicated because it is highly programmable, so I've only included the relevant code for review below.

Supply.h:

#include <utility>
#include <vector>

/** \brief Multi-port power supply.

Two adjacent ports of the power supply can be combined in parallel to increase the output power capability.
*/
class Supply {
public:
    /** \brief Type definition for a number assigned to a Supply port. */
    typedef unsigned int port_number;

    /** \brief Type definition for a number assigned to a Supply output. */
    typedef port_number output_number;

    /** \brief Power supply output which can be connected to a load.

    An output can be a single (one port) or combined (two ports in parallel) connection.
    */
    class Output {
        friend class Supply;
    public:
        /**\brief Enumeration of the port wiring options. */
        enum output_type {
            single = 1,     /**< Output consists of only one port. */
            combined = 2    /**< Output consists of two ports in paralle. */
        };

        /**\brief Special port number used to indicate that the secondary port is not in use (i.e. the
        Output is of type Supply::Output::single and only uses the primary). */
        const static port_number no_port = 0;
    private:
        std::pair<port_number, port_number> ports; /**< \brief The port number(s) assigned to an Output. */
        output_type m_type; /**< \brief Indicates whether the Output is configured as single or combined. */

        /**\brief Assigns a port to the primary slot of the Output
        \param[in] p the port to assign to the primary slot
        \throw std::logic_error if an attempt is made to assign Supply::Output::no_port to the primary
        pairs of the Output
        \throw std::logic_error if the argument port is already assigned to the Output's secondary slot
        */
        void primary(port_number p);

        /**\brief Assigns or removes a port to the secondary slot of the Output
        \param[in] p the port to assign to the primary slot. If set to Supply::Output::no_port the secondary
        slot will be unassigned (i.e. the Output is of type Supply::Output::single).
        \throw std::logic_error if the argument port is already assigned to the Output's primary slot
        */
        void secondary(port_number p);

        /**\brief Constructs a combined Output
        \param[in] port1 the primary port
        \param[in] port2 the secondary port
        */
        Output(port_number port1, port_number port2);

        /**\brief Constructs a single Output
        \param[in] p the primary port
        */
        Output(port_number p);

    public:
        /**\brief Returns Supply::Output::single if the Output's secondary slot is unassigned (i.e. set to
        Supply::Output::no_port), Supply::Output::combined otherwise.
        */
        output_type type() const;

        /**\brief Returns the primary port */
        port_number primary() const;

        /**\brief Returns the secondary port, if it is assigned
        \throw std::out_of_range if the secondary port is not assigned
        */
        port_number secondary() const;

        bool contains(port_number p) const;
    };
private:
    /**\brief The total number of ports contained in the Supply. */
    port_number num_ports;

    /**\brief Type definition for the container of Outputs. */
    typedef std::vector<Output> output_list;

    output_list m_outputs; /**<\brief List of configured Outputs. */
public:
    /**\brief Type definition for an iterator allowing client code to access the Supply's Outputs. */
    typedef output_list::iterator output_iterator;

    /**\brief Constructs a Supply with `num_ports` ports. All ports are initialized as single Outputs. */
    Supply(port_number num_ports) : num_ports(num_ports) {
        m_outputs.reserve(num_ports);

        for (Supply::port_number p = 1; p <= num_ports; ++p) {
            m_outputs.push_back(Supply::Output(p));
        }
    }

    /**\brief Returns an iterator to the first Output.

    Outputs are sorted so that the first one has the lowest port number assigned (either as primary
    or secondary).
    */
    output_iterator first_output() {
        return m_outputs.begin();
    }

    /**\brief Returns an iterator to the end of the Output list.

    Outputs are sorted so that the first one has the lowest port number assigned (either as primary
    or secondary).
    */
    output_iterator last_output() {
        return m_outputs.end();
    }

    /**\brief Returns a reference to the Output at specified location `n`, with bounds checking.
    \throw std::out_of_range if `pos` is not within the range of Outputs contained by the Supply.
    */
    const Output& output(output_number n) const {
        // n starts with 1, outputs are indexed starting with 0
        return m_outputs.at(n - 1);
    }

    /**\brief Returns a reference to the Output at specified location `n`. No bounds checking is
    performed.
    */
    const Output& operator[](output_number n) const {
        // n starts with 1, outputs are indexed starting with 0
        return m_outputs[n - 1];
    }

    /**\brief Returns the number of ports supported by the Supply.

    This number never changes during the lifetime of a Supply. */
    port_number ports() const { return num_ports; }

    /**\brief Returns the number of Outputs configured for the Supply.

    Depending on the configuration of the Outputs, there may be as many as one Output per Supply port
    (all Outputs are of type Supply::Output::single) or as few as half the number of Outputs as
    number of ports in the Supply (all Outputs are of type Supply::Output::combined).
    */
    output_number outputs() const { return static_cast<output_number>(m_outputs.size()); }

    // NOTE: would actually be private, and called when a triggering event causes the Supply to call this function
    /**\brief Creates a combined Output using the given two ports.

    Both `port1` and `port2` must be in the Supply as an Output of type Supply::Output::single, and `port1` cannot
    be the same port as `port2`.
    \throw std::logic_error if `port1 == port2`.
    \throw std::logic_error if `port1` does not exist in the Supply as an Output of type Supply::Output::single
    \throw std::logic_error if `port2` does not exist in the Supply as an Output of type Supply::Output::single
    */
    void combine_ports(port_number port1, port_number port2);

    // NOTE: would actually be private, and called when a triggering event causes the Supply to call this function
    /**\brief Splits a combined Output composed of the given two ports into two single Outputs.

    Both `port1` and `port2` must be in an Output of type Supply::Output::combined (though either can
    be the primary), and `port1` cannot be the same port as `port2`.
    \throw std::logic_error if `port1 == port2`.
    \throw std::logic_error if the Supply does not have an Output of type Supply::Output::combined which is
    composed of the two given ports.
    */
    void split_ports(port_number port1, port_number port2);
};

/**\brief Returns `true` if the argument Supply::Outputs are considered equal, `false` otherwise.

Two Supply::Outputs are considered equal if:
 * both are Supply::Output::single and the same port is assigned to both primaries.
 * both are Supply::Output::combined and the same port is assigned to both primaries and the same port
   is assigned to both secondaries.
*/
inline bool operator==(const Supply::Output& lhs, const Supply::Output& rhs) {
    if (lhs.primary() != rhs.primary()) return false;

    Supply::Output::output_type pairs = lhs.type();

    if (pairs == Supply::Output::single) {
        // lhs is single so lhs == rhs if rhs is also single
        return rhs.type() == Supply::Output::single;
    } else {
        // lhs is combined so lhs == rhs if rhs is combined and lhs.secondary() == rhs.secondary()
        if (rhs.type() != Supply::Output::combined) return false;
        else {
            // both are combined, make sure the secondaries match
            return lhs.secondary() == rhs.secondary();
        }
    }
}

/**\brief Returns `true` if the argument Supply::Outputs are considered unequal, `false` otherwise.

See operator==() for rules regarding the equality of Supply::Outputs.
*/
inline bool operator!=(const Supply::Output& lhs, const Supply::Output& rhs) {
    return !(lhs == rhs);
}

/**\brief Returns `true` if the argument Supply::Outputs are considered equivalent, `false` otherwise.

Not as strict as operator==(). Two Supply::Outputs are equivalent if
 * both are Supply::Output::single and the same port is assigned to both primaries.
 * both are Supply::Output::combined and the same two port numbers are assigned. The difference from
   operator==() is that the ports can be assigned to opposite pairs.
*/
inline bool equivalent(const Supply::Output& lhs, const Supply::Output& rhs) {
    if (lhs.type() != rhs.type()) return false;

    if (lhs.type() == Supply::Output::single) {
        return lhs.primary() == rhs.primary();
    } else {
        // both lhs and rhs must be combined so check that the same two port numbers are used
        return (lhs.primary() == rhs.primary() || lhs.primary() == rhs.secondary()) &&
            (lhs.secondary() == rhs.primary() || lhs.secondary() == rhs.secondary());
    }
}

/**\brief Returns `true` if the Supply::Output on the left-hand side is compares less than the one
on the right-hand side.

One Supply::Output is considered less than another if it has a port number assigned to it that is less
than the port(s) assigned to the other.
*/
inline bool operator<(const Supply::Output& lhs, const Supply::Output& rhs) {
    Supply::port_number lhs_min = lhs.primary();
    Supply::port_number rhs_min = rhs.primary();

    if (lhs.type() == Supply::Output::combined) lhs_min = std::min(lhs_min, lhs.secondary());
    if (rhs.type() == Supply::Output::combined) rhs_min = std::min(rhs_min, rhs.secondary());

    return lhs_min < rhs_min;
}

Supply.cpp:

#include <algorithm>
#include <exception>

#include "Supply.h"

Supply::Output::Output(Supply::port_number port1, Supply::port_number port2) {
    ports.first = port1;
    ports.second = port2;
    m_type = combined;
}

Supply::Output::Output(port_number p) {
    ports.first = p;
    ports.second = no_port;
    m_type = single;
}

Supply::port_number Supply::Output::primary() const {
    return ports.first;
}

Supply::port_number Supply::Output::secondary() const {
    // If single, just return no_port without throwing an exception?
    if (m_type == single) throw std::out_of_range("Can't access secondary port in a two-pair Supply::Output");
    else return ports.second;
}

void Supply::Output::primary(Supply::port_number p) {
    if (p == no_port) throw std::logic_error("Supply::Output must have a port assigned to its primary");

    if (m_type == combined && p == ports.second) {
        throw std::logic_error("Supply::Output cannot use the same port for both the primary and secondary");
    }

    ports.first = p;
}

void Supply::Output::secondary(Supply::port_number p) {
    if (p == ports.first) {
        throw std::logic_error("Supply::Output cannot use the same port for both the primary and secondary");
    }

    ports.second = p;

    if (p == no_port) m_type = single;
    else m_type = combined;
}

Supply::Output::output_type Supply::Output::type() const { return m_type; }

bool Supply::Output::contains(Supply::port_number p) const {
    if (primary() == p) return true;

    if (m_type == combined) return (secondary() == p);
    else return false;
}

void Supply::combine_ports(Supply::port_number port1, Supply::port_number port2) {
    if (port1 == port2) throw std::logic_error("port1 cannot equal port2");

    output_list::iterator iter1 = m_outputs.end();
    output_list::iterator iter2 = m_outputs.end();

    for (output_list::iterator iter = m_outputs.begin(); iter != m_outputs.end(); ++iter) {
        Supply::Output& o = *iter;

        if (o.type() == Supply::Output::single && o.primary() == port1) iter1 = iter;
        if (o.type() == Supply::Output::single && o.primary() == port2) iter2 = iter;
    }

    if (iter1 == m_outputs.end()) throw std::logic_error("Could not find port1");
    if (iter2 == m_outputs.end()) throw std::logic_error("Could not find port2");

    if (port1 < port2) {
        Supply::Output& o = *iter1;
        o.secondary(port2); // add port2 to the Output that port1 is already part of
        m_outputs.erase(iter2); // remove the Output that port2 was part of
    } else {
        Supply::Output& o = *iter2;
        o.secondary(port1); // add port1 to the Output that port2 is already part of
        m_outputs.erase(iter1); // remove the Output that port1 was part of
    }

    std::sort(m_outputs.begin(), m_outputs.end());
}

void Supply::split_ports(Supply::port_number port1, Supply::port_number port2) {
    if (port1 == port2) throw std::logic_error("port1 cannot equal port2");

    Supply::Output otest(port1, port2); // construct a combined Output to test against for equality

    bool found = false; // set to true if/when the combined Output with the two argument ports is found

    for (output_list::iterator iter = m_outputs.begin(); iter != m_outputs.end(); ++iter) {
        Supply::Output& o = *iter;

        if (o.type() == Supply::Output::combined && equivalent(o, otest)) {
            found = true;
            m_outputs.erase(iter);
            break;
        }
    }

    if (!found) {
        throw std::logic_error("Could not find a combined Output with port1 and port2");
    }

    m_outputs.push_back(Supply::Output(port1));
    m_outputs.push_back(Supply::Output(port2));

    std::sort(m_outputs.begin(), m_outputs.end());
}

Some important notes:

  1. Supply::split_ports() and Supply::combine_ports() would be private in the real class since the code that controls the Supply does not directly control this feature (it communicates with the power supply using I2C, and the appropriate I2C command controls this feature); I've made these functions public for demonstration purposes only.
  2. Port numbers and output numbers start with 1, not 0 as is usual in C++ for accessing the first element in a std::vector.
  3. I've included the < comparison operator for sorting Outputs, but for brevity I haven't included >, etc.
  4. My compiler at work is the aging VS2005 compiler so I can't use C++11 or beyond. Feel free to make suggestions that would apply for a more modern compiler, just note that I won't be able to use them at this time.

I've made Output a nested class since an Output only makes sense in the context of a Supply. Also, Supply is a friend of Output so that Supply can create and modify Output objects. The only public functions of Output are const getter functions so Output is immutable to all code except Supply.

Here's a usage example:

#include <iostream>
#include <fstream>

#include "Supply.h"

void iterate_outputs(std::ostream& os, Supply supply) {
    for (Supply::output_iterator iter = supply.first_output(); iter!= supply.last_output(); ++iter) {
        if (iter->type() == Supply::Output::single) {
            os << "single output: {" << iter->primary() << "}\n";
        } else {
            os << "combined output: {" << iter->primary() << ", " << iter->secondary() << "}\n";
        }
    }

    os << "There are " << supply.ports() << " ports and " << supply.outputs() << " outputs.\n\n";
}

int main() {
    std::fstream fs("output.txt");
    std::ostream& os = fs; // or std::cout
    os << std::boolalpha;

    Supply supply(8);

    os << "Number of ports = " << supply.ports() << '\n';
    os << "Iterating through ports:\n";

    for (Supply::port_number p = 1; p <= supply.ports(); ++p) {
        os << "this is port " << p << '\n';
    }

    os << '\n';
    os << "Iterating through single outputs:\n";

    iterate_outputs(os, supply);

    supply.combine_ports(5, 6);
    supply.combine_ports(1, 2);
    supply.combine_ports(3, 4);
    supply.combine_ports(7, 8);

    os << "Iterating through combined outputs:\n";
    iterate_outputs(os, supply);

    supply.split_ports(6, 5);
    supply.split_ports(1, 2);

    os << "Iterating through mixed single/combined outputs:\n";
    iterate_outputs(os, supply);

    os << "Third output is ";
    const Supply::Output& o3 = supply[3];

    if (o3.type() == Supply::Output::single) {
        os << "single output: {" << o3.primary() << "}\n";
    } else {
        os << "combined output: {" << o3.primary() << ", " << o3.secondary() << "}\n";
    }

    const Supply::Output& o4 = supply.output(4);

    if (o4.type() == Supply::Output::single) {
        os << "single output: {" << o4.primary() << "}\n";
    } else {
        os << "combined output: {" << o4.primary() << ", " << o4.secondary() << "}\n";
    }

    os << "Fourth output contains port 4?: " << o4.contains(4) << '\n';
    os << "Fourth output contains port 5?: " << o4.contains(5) << '\n';
}

The output of this example is:

Number of ports = 8
Iterating through ports:
this is port 1
this is port 2
this is port 3
this is port 4
this is port 5
this is port 6
this is port 7
this is port 8

Iterating through single outputs:
single output: {1}
single output: {2}
single output: {3}
single output: {4}
single output: {5}
single output: {6}
single output: {7}
single output: {8}
There are 8 ports and 8 outputs.

Iterating through combined outputs:
combined output: {1, 2}
combined output: {3, 4}
combined output: {5, 6}
combined output: {7, 8}
There are 8 ports and 4 outputs.

Iterating through mixed single/combined outputs:
single output: {1}
single output: {2}
combined output: {3, 4}
single output: {5}
single output: {6}
combined output: {7, 8}
There are 8 ports and 6 outputs.

Third output is combined output: {3, 4}
single output: {5}
Fourth output contains port 4?: false
Fourth output contains port 5?: true

I'm looking for feedback on all aspects of the design, though I'm particularly interested in feedback regarding the following:

  1. Should Supply::Output::secondary() throw an exception when attempting to access a secondary port on a single-port output? Or return Supply::Output::no_port instead?
  2. Should I provide a const_iterator for the outputs in addition to an iterator? Currently I just provide an iterator since code outside of Supply can't modify an Output.
  3. Is Supply::Output::m_type useful or should I just determine whether the Output is single or combined on the fly by checking if the secondary port is set to Supply::Output::no_port?
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.