diff --git a/common/include/villas/cpuset.hpp b/common/include/villas/cpuset.hpp new file mode 100644 index 000000000..a1d787a05 --- /dev/null +++ b/common/include/villas/cpuset.hpp @@ -0,0 +1,200 @@ +/** Human readable cpusets. + * + * @file + * @author Steffen Vogel + * @copyright 2017-2018, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLAScommon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#pragma once + +#ifdef __linux__ + +#include +#include + +#include + +namespace villas { +namespace utils { + +class CpuSet { + +protected: + cpu_set_t *setp; + + unsigned num_cpus; + size_t sz; + +public: + + CpuSet() : + num_cpus(sizeof(uintmax_t) * 8), + sz(CPU_ALLOC_SIZE(num_cpus)) + { + + setp = CPU_ALLOC(num_cpus); + if (!setp) + throw new villas::RuntimeError("Failed to allocated memory"); + + zero(); + } + + /** Parses string with list of CPU ranges. + * + * @param str Human readable representation of the set. + */ + CpuSet(const std::string &str); + + /** Convert integer to cpu_set_t. + * + * @param set An integer number which is used as the mask + */ + CpuSet(uintmax_t set); + + /** Convert cpu_set_t to an integer. */ + operator uintmax_t(); + + operator const cpu_set_t*() + { + return setp; + } + + /** Returns human readable representation of the cpuset. + * + * The output format is a list of CPUs with ranges (for example, "0,1,3-9"). + */ + operator std::string(); + + ~CpuSet() + { + CPU_FREE(setp); + } + + CpuSet(const CpuSet &src) : + CpuSet(src.num_cpus) + { + memcpy(setp, src.setp, sz); + } + + bool empty() const + { + return count() == 0; + } + + bool full() const + { + return count() == num_cpus; + } + + unsigned count() const + { + return CPU_COUNT_S(sz, setp); + } + + void zero() + { + CPU_ZERO_S(sz, setp); + } + + size_t size() const + { + return sz; + } + + CpuSet operator~() + { + CpuSet full = UINTMAX_MAX; + + return full ^ *this; + } + + bool operator==(const CpuSet &rhs) + { + return CPU_EQUAL_S(sz, setp, rhs.setp); + } + + CpuSet& operator&=(const CpuSet &rhs) + { + CPU_AND_S(sz, setp, setp, rhs.setp); + return *this; + } + + CpuSet& operator|=(const CpuSet &rhs) + { + CPU_OR_S(sz, setp, setp, rhs.setp); + return *this; + } + + CpuSet& operator^=(const CpuSet &rhs) + { + CPU_XOR_S(sz, setp, setp, rhs.setp); + return *this; + } + + friend CpuSet operator&(CpuSet lhs, const CpuSet &rhs) + { + lhs &= rhs; + return lhs; + } + + friend CpuSet operator|(CpuSet lhs, const CpuSet &rhs) + { + lhs |= rhs; + return lhs; + } + + friend CpuSet operator^(CpuSet lhs, const CpuSet &rhs) + { + lhs ^= rhs; + return lhs; + } + + //bool& operator[](std::size_t cpu) + //{ + // void cpuset_set_cpu(cpuset_t*setp, cpu_t cpu, int state) + //} + + bool operator[](size_t cpu) const + { + return isset(cpu); + } + + bool isset(size_t cpu) const + { + return CPU_ISSET_S(cpu, sz, setp); + } + + void clear(size_t cpu) + { + CPU_CLR_S(cpu, sz, setp); + } + + void set(size_t cpu) + { + CPU_SET_S(cpu, sz, setp); + } +}; + +#endif + +} // namespace villas +} // namespace utils + + +#include diff --git a/common/include/villas/utils.h b/common/include/villas/utils.h index 96547713a..67ea6db69 100644 --- a/common/include/villas/utils.h +++ b/common/include/villas/utils.h @@ -27,7 +27,6 @@ #include #include #include -#include #include #include @@ -37,6 +36,8 @@ extern "C" { #endif +extern pthread_t main_thread; + #ifdef __GNUC__ #define LIKELY(x) __builtin_expect((x),1) #define UNLIKELY(x) __builtin_expect((x),0) @@ -137,15 +138,6 @@ extern "C" { #define BITMASK(h, l) (((~0ULL) << (l)) & (~0ULL >> (BITS_PER_LONGLONG - 1 - (h)))) #define BIT(nr) (1UL << (nr)) -/* Forward declarations */ -struct timespec; - -/** Print copyright message to stdout. */ -void print_copyright(); - -/** Print version to stdout. */ -void print_version(); - /** Normal random variate generator using the Box-Muller method * * @param m Mean @@ -178,38 +170,6 @@ char * vstrcatf(char **dest, const char *fmt, va_list va) #define strf(fmt, ...) strcatf(&(char *) { NULL }, fmt, ##__VA_ARGS__) #define vstrf(fmt, va) vstrcatf(&(char *) { NULL }, fmt, va) -#ifdef __linux__ -/** Convert integer to cpu_set_t. - * - * @param set An integer number which is used as the mask - * @param cset A pointer to the cpu_set_t datastructure - */ -void cpuset_from_integer(uintmax_t set, cpu_set_t *cset); - -/** Convert cpu_set_t to an integer. */ -void cpuset_to_integer(cpu_set_t *cset, uintmax_t *set); - -/** Parses string with list of CPU ranges. - * - * From: https://github.com/mmalecki/util-linux/blob/master/lib/cpuset.c - * - * @retval 0 On success. - * @retval 1 On error. - * @retval 2 If fail is set and a cpu number passed in the list doesn't fit - * into the cpu_set. If fail is not set cpu numbers that do not fit are - * ignored and 0 is returned instead. - */ -int cpulist_parse(const char *str, cpu_set_t *set, int fail); - -/** Returns human readable representation of the cpuset. - * - * From: https://github.com/mmalecki/util-linux/blob/master/lib/cpuset.c - * - * The output format is a list of CPUs with ranges (for example, "0,1,3-9"). - */ -char * cpulist_create(char *str, size_t len, cpu_set_t *set); -#endif - /** Allocate and initialize memory. */ void * alloc(size_t bytes); @@ -219,18 +179,6 @@ void * memdup(const void *src, size_t bytes); /** Call quit() in the main thread. */ void die(); -/** Used by version_parse(), version_compare() */ -struct version { - int major; - int minor; -}; - -/** Compare two versions. */ -int version_cmp(struct version *a, struct version *b); - -/** Parse a dotted version string. */ -int version_parse(const char *s, struct version *v); - /** Check assertion and exit if failed. */ #ifndef assert #define assert(exp) do { \ @@ -240,9 +188,6 @@ int version_parse(const char *s, struct version *v); } while (0) #endif -/** Fill buffer with random data */ -ssize_t read_random(char *buf, size_t len); - /** Get log2 of long long integers */ static inline int log2i(long long x) { if (x == 0) @@ -251,9 +196,6 @@ static inline int log2i(long long x) { return sizeof(x) * 8 - __builtin_clzll(x) - 1; } -/** Register a exit callback for program termination: SIGINT, SIGKILL & SIGALRM. */ -int signals_init(void (*cb)(int signal, siginfo_t *sinfo, void *ctx)); - /** Send signal \p sig to main thread. */ void killme(int sig); @@ -262,10 +204,6 @@ pid_t spawn(const char *name, char *const argv[]); /** Determines the string length as printed on the screen (ignores escable sequences). */ size_t strlenp(const char *str); -/** Remove ANSI control sequences for colored output. */ -char * decolor(char *str); - - #ifdef __cplusplus } #endif diff --git a/common/include/villas/utils.hpp b/common/include/villas/utils.hpp index 1abc24078..7a81fbe81 100644 --- a/common/include/villas/utils.hpp +++ b/common/include/villas/utils.hpp @@ -1,6 +1,7 @@ /** Utilities. * * @file + * @author Steffen Vogel * @author Daniel Krebs * @copyright 2017-2018, Institute for Automation of Complex Power Systems, EONERC * @license GNU General Public License (version 3) @@ -26,6 +27,8 @@ #include #include +#include + namespace villas { namespace utils { @@ -41,6 +44,21 @@ assertExcept(bool condition, const T& exception) throw exception; } +/** Print copyright message to stdout. */ +void print_copyright(); + +/** Print version to stdout. */ +void print_version(); + +/** Register a exit callback for program termination: SIGINT, SIGKILL & SIGALRM. */ +int signals_init(void (*cb)(int signal, siginfo_t *sinfo, void *ctx)); + +/** Fill buffer with random data */ +ssize_t read_random(char *buf, size_t len); + +/** Remove ANSI control sequences for colored output. */ +char * decolor(char *str); + } // namespace utils } // namespace villas diff --git a/common/include/villas/version.hpp b/common/include/villas/version.hpp new file mode 100644 index 000000000..71aa3a675 --- /dev/null +++ b/common/include/villas/version.hpp @@ -0,0 +1,53 @@ +/** Version. + * + * @file + * @author Steffen Vogel + * @copyright 2017-2018, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLAScommon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#pragma once + +#include + +namespace villas { +namespace utils { + +class Version { + +protected: + int components[3]; + + static int cmp(const Version& lhs, const Version& rhs); + +public: + /** Parse a dotted version string. */ + Version(const std::string &s); + + Version(int maj, int min = 0, int pat = 0); + + inline bool operator==(const Version& rhs) { return cmp(*this, rhs) == 0; } + inline bool operator!=(const Version& rhs) { return cmp(*this, rhs) != 0; } + inline bool operator< (const Version& rhs) { return cmp(*this, rhs) < 0; } + inline bool operator> (const Version& rhs) { return cmp(*this, rhs) > 0; } + inline bool operator<=(const Version& rhs) { return cmp(*this, rhs) <= 0; } + inline bool operator>=(const Version& rhs) { return cmp(*this, rhs) >= 0; } +}; + +} // namespace villas +} // namespace utils diff --git a/common/lib/CMakeLists.txt b/common/lib/CMakeLists.txt index 43847336f..05f78f285 100644 --- a/common/lib/CMakeLists.txt +++ b/common/lib/CMakeLists.txt @@ -44,6 +44,8 @@ add_library(villas-common SHARED tsc.c utils.c utils.cpp + cpuset.cpp + version.cpp ) if(CMAKE_SYSTEM_NAME STREQUAL Linux) diff --git a/common/lib/cpuset.cpp b/common/lib/cpuset.cpp new file mode 100644 index 000000000..9f05573d8 --- /dev/null +++ b/common/lib/cpuset.cpp @@ -0,0 +1,127 @@ +/** Human readable cpusets. + * + * @file + * @author Steffen Vogel + * @copyright 2017-2018, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLAScommon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include + +using namespace villas::utils; + +#ifdef __linux__ + +CpuSet::operator uintmax_t() +{ + uintmax_t iset = 0; + + for (size_t i = 0; i < num_cpus; i++) { + if (isset(i)) + iset |= 1ULL << i; + } + + return iset; +} + +CpuSet::CpuSet(uintmax_t iset) : + CpuSet() +{ + zero(); + + for (size_t i = 0; i < num_cpus; i++) { + if (iset & (1L << i)) + set(i); + } +} + +CpuSet::CpuSet(const std::string &str) : + CpuSet() +{ + size_t endpos, start, end; + + for (auto token : tokenize(str, ",")) { + auto sep = token.find('-'); + + if (sep == std::string::npos) { + start = std::stoi(token, &endpos); + + if (token.begin() + endpos != token.end()) + throw new std::invalid_argument("Not a valid CPU set"); + + if (start < num_cpus) + set(start); + } + else { + start = std::stoi(token, &endpos); + + if (token.begin() + endpos != token.begin() + sep) + throw new std::invalid_argument("Not a valid CPU set"); + + auto token2 = token.substr(endpos + 1); + + end = std::stoi(token2, &endpos); + + if (token2.begin() + endpos != token2.end()) + throw new std::invalid_argument("Not a valid CPU set"); + + for (size_t i = start; i <= end && i < num_cpus; i++) + set(i); + } + } +} + +CpuSet::operator std::string () +{ + std::stringstream ss; + + bool first = true; + + for (size_t i = 0; i < num_cpus; i++) { + if (isset(i)) { + size_t run = 0; + for (size_t j = i + 1; j < num_cpus; j++) { + if (!isset(j)) + break; + + run++; + } + + if (first) + first = false; + else + ss << ","; + + ss << i; + + if (run == 1) { + ss << "," << (i + 1); + i++; + } + else if (run > 1) { + ss << "-" << (i + run); + i += run; + } + } + } + + return ss.str(); +} + +#endif /* __linux__ */ diff --git a/common/lib/utils.c b/common/lib/utils.c index 66883678a..b2f999fea 100644 --- a/common/lib/utils.c +++ b/common/lib/utils.c @@ -23,44 +23,20 @@ #include #include #include +#include #include #include #include #include #include -#include #include +#include #include #include pthread_t main_thread; -void print_copyright() -{ - printf(PROJECT_NAME " %s (built on %s %s)\n", - CLR_BLU(PROJECT_BUILD_ID), CLR_MAG(__DATE__), CLR_MAG(__TIME__)); - printf(" Copyright 2014-2017, Institute for Automation of Complex Power Systems, EONERC\n"); - printf(" Steffen Vogel \n"); -} - -void print_version() -{ - printf("%s\n", PROJECT_BUILD_ID); -} - -int version_parse(const char *s, struct version *v) -{ - return sscanf(s, "%u.%u", &v->major, &v->minor) != 2; -} - -int version_cmp(struct version *a, struct version *b) { - int major = a->major - b->major; - int minor = a->minor - b->minor; - - return major ? major : minor; -} - double box_muller(float m, float s) { double x1, x2, y1; @@ -118,128 +94,6 @@ char * vstrcatf(char **dest, const char *fmt, va_list ap) return *dest; } -#ifdef __linux__ - -void cpuset_to_integer(cpu_set_t *cset, uintmax_t *set) -{ - *set = 0; - for (int i = 0; i < MIN(sizeof(*set) * 8, CPU_SETSIZE); i++) { - if (CPU_ISSET(i, cset)) - *set |= 1ULL << i; - } -} - -void cpuset_from_integer(uintmax_t set, cpu_set_t *cset) -{ - CPU_ZERO(cset); - for (int i = 0; i < MIN(sizeof(set) * 8, CPU_SETSIZE); i++) { - if (set & (1L << i)) - CPU_SET(i, cset); - } -} - -/* From: https://github.com/mmalecki/util-linux/blob/master/lib/cpuset.c */ -static const char *nexttoken(const char *q, int sep) -{ - if (q) - q = strchr(q, sep); - if (q) - q++; - return q; -} - -int cpulist_parse(const char *str, cpu_set_t *set, int fail) -{ - const char *p, *q; - int r = 0; - - q = str; - CPU_ZERO(set); - - while (p = q, q = nexttoken(q, ','), p) { - unsigned int a; /* beginning of range */ - unsigned int b; /* end of range */ - unsigned int s; /* stride */ - const char *c1, *c2; - char c; - - if ((r = sscanf(p, "%u%c", &a, &c)) < 1) - return 1; - b = a; - s = 1; - - c1 = nexttoken(p, '-'); - c2 = nexttoken(p, ','); - if (c1 != NULL && (c2 == NULL || c1 < c2)) { - if ((r = sscanf(c1, "%u%c", &b, &c)) < 1) - return 1; - c1 = nexttoken(c1, ':'); - if (c1 != NULL && (c2 == NULL || c1 < c2)) { - if ((r = sscanf(c1, "%u%c", &s, &c)) < 1) - return 1; - if (s == 0) - return 1; - } - } - - if (!(a <= b)) - return 1; - while (a <= b) { - if (fail && (a >= CPU_SETSIZE)) - return 2; - CPU_SET(a, set); - a += s; - } - } - - if (r == 2) - return 1; - - return 0; -} - -char *cpulist_create(char *str, size_t len, cpu_set_t *set) -{ - size_t i; - char *ptr = str; - int entry_made = 0; - - for (i = 0; i < CPU_SETSIZE; i++) { - if (CPU_ISSET(i, set)) { - int rlen; - size_t j, run = 0; - entry_made = 1; - for (j = i + 1; j < CPU_SETSIZE; j++) { - if (CPU_ISSET(j, set)) - run++; - else - break; - } - if (!run) - rlen = snprintf(ptr, len, "%zd,", i); - else if (run == 1) { - rlen = snprintf(ptr, len, "%zd,%zd,", i, i + 1); - i++; - } else { - rlen = snprintf(ptr, len, "%zd-%zd,", i, i + run); - i += run; - } - if (rlen < 0 || (size_t) rlen + 1 > len) - return NULL; - ptr += rlen; - if (rlen > 0 && len > (size_t) rlen) - len -= rlen; - else - len = 0; - } - } - ptr -= entry_made; - *ptr = '\0'; - - return str; -} -#endif /* __linux__ */ - void * alloc(size_t bytes) { void *p = malloc(bytes); @@ -260,70 +114,6 @@ void * memdup(const void *src, size_t bytes) return dst; } -ssize_t read_random(char *buf, size_t len) -{ - int fd; - ssize_t bytes, total; - - fd = open("/dev/urandom", O_RDONLY); - if (fd < 0) - return -1; - - bytes = 0; - total = 0; - while (total < len) { - bytes = read(fd, buf + total, len - total); - if (bytes < 0) - break; - - total += bytes; - } - - close(fd); - - return bytes; -} - -/* Setup exit handler */ -int signals_init(void (*cb)(int signal, siginfo_t *sinfo, void *ctx)) -{ - int ret; - - info("Initialize signals"); - - struct sigaction sa_quit = { - .sa_flags = SA_SIGINFO | SA_NODEFER, - .sa_sigaction = cb - }; - - struct sigaction sa_chld = { - .sa_flags = 0, - .sa_handler = SIG_IGN - }; - - main_thread = pthread_self(); - - sigemptyset(&sa_quit.sa_mask); - - ret = sigaction(SIGINT, &sa_quit, NULL); - if (ret) - return ret; - - ret = sigaction(SIGTERM, &sa_quit, NULL); - if (ret) - return ret; - - ret = sigaction(SIGALRM, &sa_quit, NULL); - if (ret) - return ret; - - ret = sigaction(SIGCHLD, &sa_chld, NULL); - if (ret) - return ret; - - return 0; -} - void killme(int sig) { /* Send only to main thread in case the ID was initilized by signals_init() */ @@ -384,36 +174,3 @@ size_t strlenp(const char *str) return sz; } - -char * decolor(char *str) -{ - char *p, *q; - bool inseq = false; - - for (p = q = str; *p; p++) { - switch (*p) { - case 0x1b: - if (*(++p) == '[') { - inseq = true; - continue; - } - break; - - case 'm': - if (inseq) { - inseq = false; - continue; - } - break; - } - - if (!inseq) { - *q = *p; - q++; - } - } - - *q = '\0'; - - return str; -} diff --git a/common/lib/utils.cpp b/common/lib/utils.cpp index df8782a28..52eaaa766 100644 --- a/common/lib/utils.cpp +++ b/common/lib/utils.cpp @@ -22,8 +22,14 @@ #include #include +#include +#include +#include + +#include #include +#include namespace villas { namespace utils { @@ -53,5 +59,112 @@ tokenize(std::string s, std::string delimiter) return tokens; } +void print_copyright() +{ + std::cout << PROJECT_NAME " " << CLR_BLU(PROJECT_BUILD_ID) + << " (built on " CLR_MAG(__DATE__) " " CLR_MAG(__TIME__) ")" << std::endl + << " Copyright 2014-2017, Institute for Automation of Complex Power Systems, EONERC" << std::endl + << " Steffen Vogel " << std::endl; +} + +void print_version() +{ + std::cout << PROJECT_BUILD_ID << std::endl; +} + +ssize_t read_random(char *buf, size_t len) +{ + int fd; + ssize_t bytes; + + fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) + return -1; + + while (len) { + bytes = read(fd, buf, len); + if (bytes < 0) + break; + + len -= bytes; + buf += bytes; + } + + close(fd); + + return bytes; +} + +/* Setup exit handler */ +int signals_init(void (*cb)(int signal, siginfo_t *sinfo, void *ctx)) +{ + int ret; + + info("Initialize signals"); + + struct sigaction sa_quit; + sa_quit.sa_flags = SA_SIGINFO | SA_NODEFER; + sa_quit.sa_sigaction = cb; + + struct sigaction sa_chld; + sa_chld.sa_flags = 0; + sa_chld.sa_handler = SIG_IGN; + + main_thread = pthread_self(); + + sigemptyset(&sa_quit.sa_mask); + + ret = sigaction(SIGINT, &sa_quit, NULL); + if (ret) + return ret; + + ret = sigaction(SIGTERM, &sa_quit, NULL); + if (ret) + return ret; + + ret = sigaction(SIGALRM, &sa_quit, NULL); + if (ret) + return ret; + + ret = sigaction(SIGCHLD, &sa_chld, NULL); + if (ret) + return ret; + + return 0; +} + +char * decolor(char *str) +{ + char *p, *q; + bool inseq = false; + + for (p = q = str; *p; p++) { + switch (*p) { + case 0x1b: + if (*(++p) == '[') { + inseq = true; + continue; + } + break; + + case 'm': + if (inseq) { + inseq = false; + continue; + } + break; + } + + if (!inseq) { + *q = *p; + q++; + } + } + + *q = '\0'; + + return str; +} + } // namespace utils } // namespace villas diff --git a/common/lib/version.cpp b/common/lib/version.cpp new file mode 100644 index 000000000..66970b342 --- /dev/null +++ b/common/lib/version.cpp @@ -0,0 +1,70 @@ +/** Version. + * + * @author Steffen Vogel + * @copyright 2017-2018, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLAScommon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include + +#include +#include +#include + +using namespace villas::utils; + +Version::Version(const std::string &str) +{ + size_t endpos; + + auto comp = tokenize(str, "."); + + if (comp.size() > 3) + throw new std::invalid_argument("Not a valid version string"); + + for (unsigned i = 0; i < 3; i++) { + if (i < comp.size()) { + components[i] = std::stoi(comp[i], &endpos, 10); + + if (comp[i].begin() + endpos != comp[i].end()) + throw new std::invalid_argument("Not a valid version string"); + } + else + components[i] = 0; + } +} + +Version::Version(int maj, int min, int pat) : + components{maj, min, pat} +{ + +} + +int Version::cmp(const Version& lhs, const Version& rhs) +{ + int d; + + for (int i = 0; i < 3; i++) { + d = lhs.components[i] - rhs.components[i]; + if (d) + return d; + } + + return 0; +}