|
| 1 | +# Cpp Expression Tree |
| 2 | + |
| 3 | +Cpp Expression Tree is a header-only, C++14 library for creating logical expression trees and using them to evaluate instances of user-defined data types. |
| 4 | + |
| 5 | +Inspired by m-peko/booleval. |
| 6 | + |
| 7 | +This project is under development and is subject to change. Project contributions and issue reports are welcome. The more the merrier! |
| 8 | +( ... well, maybe not so much for bug reports) |
| 9 | + |
| 10 | + |
| 11 | +## Table of Contents |
| 12 | + |
| 13 | +* [A Quick Example](#a-quick-example) |
| 14 | +* [Creating Expression Trees](#creating-expression-trees) |
| 15 | +* [Types of Expression Tree Nodes](#types-of-expression-tree-nodes) |
| 16 | + * [Expression Tree Leaf Nodes](#expression-tree-leaf-nodes) |
| 17 | + * [Expression Tree Op Nodes](#expression-tree-op-nodes) |
| 18 | +* [Logical Operators](#logical-operators) |
| 19 | +* [Boolean Operators](#boolean-operators) |
| 20 | +* [Using this Library](#using-this-library) |
| 21 | +* [Compiling](#compiling) |
| 22 | + * [Running the Unit Tests](#running-the-unit-tests) |
| 23 | + |
| 24 | + |
| 25 | +## A Quick Example |
| 26 | + |
| 27 | +```cpp |
| 28 | +#include <attwoodn/expression_tree.hpp> |
| 29 | + |
| 30 | +using namespace attwoodn::expression_tree; |
| 31 | + |
| 32 | +// imagine there is some user-defined type, like so |
| 33 | +struct my_type { |
| 34 | + int my_int; |
| 35 | + bool my_bool; |
| 36 | + |
| 37 | + int get_my_int() const { |
| 38 | + return my_int; |
| 39 | + } |
| 40 | +}; |
| 41 | + |
| 42 | +... |
| 43 | + |
| 44 | +// the Cpp Expression Tree library can be used to create an expression tree to |
| 45 | +// evaluate instances of my_type |
| 46 | + |
| 47 | +// create an expression tree: my_bool == true OR (get_my_int() > 0 AND my_int < 10) |
| 48 | +expression_tree<my_type> expr { |
| 49 | + make_expr(&my_type::my_bool, op::equals, true) |
| 50 | + ->OR((make_expr(&my_type::get_my_int, op::greater_than, 0) |
| 51 | + ->AND(make_expr(&my_type::my_int, op::less_than, 10)) |
| 52 | + ) |
| 53 | + ) |
| 54 | +}; |
| 55 | + |
| 56 | + |
| 57 | +// create an instance of my_type that satisfies the above expression |
| 58 | +my_type obj; |
| 59 | +obj.my_bool = true; |
| 60 | +obj.my_int = 4; |
| 61 | +assert(expr.evaluate(obj)); // returns true - obj matches the expression |
| 62 | + |
| 63 | + |
| 64 | +// update obj so that my_int is outside the range 1..9 |
| 65 | +obj.my_bool = true; |
| 66 | +obj.my_int = 12; |
| 67 | +assert(expr.evaluate(obj)); // returns true - obj matches the expression |
| 68 | + |
| 69 | + |
| 70 | +// update obj so that my_bool is false and my_int is outside the range 1..9 |
| 71 | +obj.my_bool = false; |
| 72 | +obj.my_int = 0; |
| 73 | +assert(!expr.evaluate(obj)); // returns false - obj does not match the expression |
| 74 | +``` |
| 75 | +
|
| 76 | +Below is a diagram showing the content of the `expression_tree<my_type>` created in the example code above: |
| 77 | +
|
| 78 | +<p align="center"> |
| 79 | + <img src="docs/a_quick_example_expression_tree.png"/> |
| 80 | +</p> |
| 81 | +
|
| 82 | +As you can imagine, this example code can be expanded to fit a variety of use cases and user-defined types. More complex code examples are provided in the documentation below. Further, there are a number of unit tests located in the `tests` directory, which may be helpful for getting familiar with the library. |
| 83 | +
|
| 84 | +
|
| 85 | +## Creating Expression Trees |
| 86 | +
|
| 87 | +The `expression_tree` class is a templated, RAII container class that takes ownership of user-defined expressions. Instances of `expression_tree` can be moved and/or copied to different contexts while maintaining consistency and memory safety. The template parameter of `expression_tree` defines the type of object that the `expression_tree` will evaluate. Assuming there is a user-defined struct named `my_type`, the templated `expression_tree` type would look like this: `expression_tree<my_type>`. The template argument of `expression_tree` cannot be a primitive type, like `int`, `char`, or `double`. |
| 88 | +
|
| 89 | +An `expression_tree` cannot be default constructed - it must be initialized with an expression. Users can easily and intuitively define expressions using one of the `make_expr` helper functions found in the namespace `attwoodn::expression_tree`. `make_expr` generates heap-allocated pointers to expression tree nodes and returns them. As such, the returned expression tree node pointers should be managed carefully. If the returned pointers are not wrapped in an `expression_tree` or a smart pointer, they will need to be explicitly deleted by the calling code. |
| 90 | +
|
| 91 | +Here are some examples of how a user may safely handle the return value from one of the `make_expr` helper functions: |
| 92 | +```cpp |
| 93 | +#include <attwoodn/expression_tree.hpp> |
| 94 | +
|
| 95 | +using namespace attwoodn::expression_tree; |
| 96 | +
|
| 97 | +// let's bring back the same implementation of my_type as shown above |
| 98 | +struct my_type { |
| 99 | + int my_int; |
| 100 | + bool my_bool; |
| 101 | +
|
| 102 | + int get_my_int() const { |
| 103 | + return my_int; |
| 104 | + } |
| 105 | +}; |
| 106 | +
|
| 107 | +
|
| 108 | +... |
| 109 | +
|
| 110 | +
|
| 111 | +// The heap-allocated expression node pointer returned by make_expr becomes owned by the expression_tree |
| 112 | +expression_tree<my_type> expr_tree_raw { |
| 113 | + make_expr(&my_type::my_bool, op::equals, true) |
| 114 | +}; |
| 115 | + |
| 116 | +
|
| 117 | +... |
| 118 | +
|
| 119 | +
|
| 120 | +// The heap-allocated expression node pointer returned by make_expr becomes owned by the unique_ptr |
| 121 | +std::unique_ptr<node::expression_tree_node<my_type>> smart_expr { |
| 122 | + make_expr(&my_type::my_bool, op::equals, true) |
| 123 | +}; |
| 124 | +
|
| 125 | +// the expression_tree takes ownership of the unique_ptr |
| 126 | +expression_tree<my_type> expr_tree_smart(std::move(smart_expr)); |
| 127 | +
|
| 128 | +
|
| 129 | +... |
| 130 | +
|
| 131 | +
|
| 132 | +// The heap-allocated expression node pointer returned by make_expr must be explicitly deleted |
| 133 | +auto* expr_raw = make_expr(&my_type::my_bool, op::equals, true); |
| 134 | +delete expr_raw; |
| 135 | +``` |
| 136 | + |
| 137 | +The `make_expr` helper function is templated and overloaded to allow for maximum compatibility for use within expressions. There are three definitions of `make_expr`: |
| 138 | + * One that accepts a reference to a value-type class member variable, an operator function, and a comparison value (whose type matches the given class member variable); |
| 139 | + * One that accepts a reference to a pointer-type class member variable, an operator function, and a pointer to a comparison value (whose type matches the given class member variable); and |
| 140 | + * One that accepts a reference to a const class member function, an operator function, and a comparison value (whose type matches the return type of the given const class member function) |
| 141 | + |
| 142 | + |
| 143 | +Please see the section below for more information about expression tree nodes. |
| 144 | + |
| 145 | + |
| 146 | +## Types of Expression Tree Nodes |
| 147 | + |
| 148 | +There are two types of expression tree nodes: leaf nodes and op nodes. |
| 149 | + |
| 150 | +### Expression Tree Leaf Nodes |
| 151 | + |
| 152 | +Expression tree leaf nodes contain individual, actionable expressions against which a class/struct instance is evaluated. Expression tree leaf nodes are only ever found at the extremities of the expression tree. |
| 153 | + |
| 154 | +### Expression Tree Op Nodes |
| 155 | + |
| 156 | +Expression tree op nodes contain a boolean operation (AND/OR) and have references to a left child node and a right child node. The child nodes may be expression tree leaf nodes, expression tree op nodes, or a permutation of the two. Expression tree op nodes are only ever found in the inner part of the tree. An expression tree op node is always a parent node and it always has two child nodes. |
| 157 | + |
| 158 | + |
| 159 | +## Logical Operators |
| 160 | + |
| 161 | +There are several logical operator functions defined in the namespace `attwoodn::expression_tree::op` that can be used to create expression tree leaf nodes. The included operators are: |
| 162 | + * equals |
| 163 | + * not_equals |
| 164 | + * less_than |
| 165 | + * greater_than |
| 166 | + |
| 167 | +Each of the above logical operator functions are templated, and are overloaded to permit passing arguments of either a `const T&` type, or a `T*` type. This means that value types, references, and pointers are all permissible for comparison. |
| 168 | + |
| 169 | +Note that there is a known limitation to comparing `T*` types, such as `char*`, using the above operator functions. With `T*` types, no iteration is performed, so comparison is performed only on the data located at the beginning of the pointer address. For example: |
| 170 | + |
| 171 | +```cpp |
| 172 | +assert(op::equals("test", "testing 123")); // returns true |
| 173 | + |
| 174 | +assert(!op::equals(std::string("test"), std::string("testing 123"))); // returns false |
| 175 | +``` |
| 176 | +
|
| 177 | +Should users wish to compare an iterable collection of elements using the provided operator functions, they should compare container types, such as `std::vector<T>`, instead of pointer types like `T*`. |
| 178 | +
|
| 179 | +Users of the library can also easily define their own logical operators and use them when creating expressions. Here is an example of how a user might create their own operator functions and use them in an expression: |
| 180 | +
|
| 181 | +```cpp |
| 182 | +#include <attwoodn/expression_tree.hpp> |
| 183 | +
|
| 184 | +using namespace attwoodn::expression_tree; |
| 185 | +
|
| 186 | +// imagine there are two user-defined types, like so: |
| 187 | +struct packet_payload { |
| 188 | + uint16_t error_code; |
| 189 | + std::string data; |
| 190 | + bool checksum_ok; |
| 191 | +
|
| 192 | + uint64_t payload_size() const { |
| 193 | + return data.size(); |
| 194 | + } |
| 195 | +}; |
| 196 | +
|
| 197 | +// data packet contains an instance of packet_payload, which needs to be evaluated |
| 198 | +class data_packet { |
| 199 | + public: |
| 200 | + std::string sender_name; |
| 201 | + packet_payload payload; |
| 202 | +}; |
| 203 | +
|
| 204 | +
|
| 205 | +... |
| 206 | +
|
| 207 | +
|
| 208 | +// user creates their own logical operator for evaluating incoming packet_payload objects. |
| 209 | +// only the first function argument is used. The other is an "empty", ignored instance |
| 210 | +auto is_small_packet_payload = [](const packet_payload& incoming, const packet_payload&) -> bool { |
| 211 | + if(incoming.error_code == 0 && incoming.checksum_ok && incoming.payload_size() <= 10) { |
| 212 | + return true; |
| 213 | + } |
| 214 | + return false; |
| 215 | +}; |
| 216 | +
|
| 217 | +// User creates an expression that only accepts small, non-errored data packets from Jim. |
| 218 | +// The expression evaluates the packet_payload using the user-defined lambda operator created above |
| 219 | +expression_tree<data_packet> expr { |
| 220 | + make_expr(&data_packet::sender_name, op::equals, std::string("Jim")) |
| 221 | + ->AND(make_expr(&data_packet::payload, is_small_packet_payload, packet_payload())) |
| 222 | +}; |
| 223 | +
|
| 224 | +data_packet incoming_packet; |
| 225 | +
|
| 226 | +// Jim sends a small, non-errored data packet |
| 227 | +incoming_packet.sender_name = "Jim"; |
| 228 | +incoming_packet.payload.checksum_ok = true; |
| 229 | +incoming_packet.payload.data = "hello!"; |
| 230 | +incoming_packet.payload.error_code = 0; |
| 231 | +assert(expr.evaluate(incoming_packet)); // passes evaluation |
| 232 | +
|
| 233 | +// Pam sends the same packet payload |
| 234 | +incoming_packet.sender_name = "Pam"; |
| 235 | +assert(!expr.evaluate(incoming_packet)); // fails evaluation. No packets from Pam are accepted (sorry) |
| 236 | +
|
| 237 | +// Jim sends a packet with a bad checksum |
| 238 | +incoming_packet.sender_name = "Jim"; |
| 239 | +incoming_packet.payload.checksum_ok = false; |
| 240 | +assert(!expr.evaluate(incoming_packet)); // fails evaluation. Packet was from Jim, but checksum failed |
| 241 | +
|
| 242 | +// Jim sends a packet whose payload is too big |
| 243 | +incoming_packet.payload.checksum_ok = true; |
| 244 | +incoming_packet.payload.data = "Boy do I have a long story for you - so I was talking to Pam ..."; |
| 245 | +assert(!expr.evaluate(incoming_packet)); // fails evaluation. Payload was too big. Give me the TLDR, Jim |
| 246 | +
|
| 247 | +// Jim sends a small, rude packet |
| 248 | +incoming_packet.payload.data = "Dwight sux"; |
| 249 | +assert(expr.evaluate(incoming_packet)); // passes evaluation. The payload was the right size this time |
| 250 | +
|
| 251 | +// Jim sends a packet has an error code |
| 252 | +incoming_packet.payload.error_code = 404; |
| 253 | +assert(!expr.evaluate(incoming_packet)); // fails evaluation. The payload had an error code |
| 254 | +``` |
| 255 | + |
| 256 | +## Boolean Operators |
| 257 | + |
| 258 | +Boolean operators are used to chain individual expression tree nodes together. There are two boolean operators that can be used: `AND` and `OR`. These boolean operators are accessible via function calls on the expression nodes. Calling these functions generates a new expression tree node which becomes the parent of the nodes on either side of the boolean operator |
| 259 | + |
| 260 | +A complex expression tree can be created by calling these functions to chain multiple expression tree nodes together. |
| 261 | + |
| 262 | + |
| 263 | +## Using this Library |
| 264 | + |
| 265 | +To include this library in your project, simply copy the content of the `include` directory into the `include` directory of your project. That's it! Now where did I put that Staples "Easy" button...? |
| 266 | + |
| 267 | + |
| 268 | +## Compiling |
| 269 | + |
| 270 | +This project uses the CMake build system. The minimum CMake version is set to 3.10. |
| 271 | + |
| 272 | +First, clone the git repository and navigate into the local copy. Once you're there, run the following commands: |
| 273 | + |
| 274 | +``` |
| 275 | +mkdir build && cd build |
| 276 | +cmake .. |
| 277 | +make |
| 278 | +``` |
| 279 | + |
| 280 | +### Running the Unit Tests |
| 281 | + |
| 282 | +After cloning and compiling the project, navigate to the build directory that was created. Enable the `BUILD_TESTING` CMake flag if it is not already enabled. My preferred tool for setting CMake flags via the command line is `ccmake`. Simply run `ccmake ..` in the build directory to get a command line UI for modifying CMake project flags. There, you can enable or disable the `BUILD_TESTING` flag, or set the build type from Release to Debug. |
| 283 | + |
| 284 | +With the `BUILD_TESTING` flag enabled, run the following command in the build directory: |
| 285 | + |
| 286 | +``` |
| 287 | +ctest . |
| 288 | +``` |
| 289 | + |
| 290 | +CTest will execute the unit tests and provide a pass/fail indication for each one. |
| 291 | + |
| 292 | +The address sanitizer is enabled on every unit test executable. A test will fail should memory leak during test execution. |
0 commit comments