Skip to content

Commit 0c90064

Browse files
authored
Merge pull request #10 from attwoodn/add-project-level-readme
Add project level readme
2 parents f25064b + 94ff37d commit 0c90064

File tree

8 files changed

+472
-2
lines changed

8 files changed

+472
-2
lines changed

README.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<mxfile host="Electron" modified="2023-05-10T18:19:44.134Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/21.2.8 Chrome/112.0.5615.165 Electron/24.2.0 Safari/537.36" etag="A1TK9bRuSOZq5wIYVlKq" version="21.2.8" type="device">
2+
<diagram name="Page-1" id="MzqSHk1QGt0DU5yl56VU">
3+
<mxGraphModel dx="1360" dy="738" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
4+
<root>
5+
<mxCell id="0" />
6+
<mxCell id="1" parent="0" />
7+
<mxCell id="4_OO1GRJjltScIyBnV0f-2" value="&lt;font style=&quot;font-size: 15px;&quot;&gt;expression_tree&amp;lt;my_type&amp;gt;&lt;/font&gt;" style="swimlane;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;shadow=1;" vertex="1" parent="1">
8+
<mxGeometry x="80" y="95" width="550" height="435" as="geometry" />
9+
</mxCell>
10+
<mxCell id="4_OO1GRJjltScIyBnV0f-4" value="&lt;font size=&quot;1&quot; style=&quot;&quot;&gt;&lt;b style=&quot;font-size: 15px;&quot;&gt;OR&lt;/b&gt;&lt;/font&gt;" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#dae8fc;strokeColor=#6c8ebf;shadow=1;" vertex="1" parent="4_OO1GRJjltScIyBnV0f-2">
11+
<mxGeometry x="174" y="50" width="80" height="80" as="geometry" />
12+
</mxCell>
13+
<mxCell id="4_OO1GRJjltScIyBnV0f-6" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=classic;startFill=1;endArrow=none;endFill=0;edgeStyle=orthogonalEdgeStyle;curved=1;shadow=1;" edge="1" parent="4_OO1GRJjltScIyBnV0f-2" source="4_OO1GRJjltScIyBnV0f-5" target="4_OO1GRJjltScIyBnV0f-4">
14+
<mxGeometry relative="1" as="geometry" />
15+
</mxCell>
16+
<mxCell id="4_OO1GRJjltScIyBnV0f-5" value="&lt;font style=&quot;font-size: 15px;&quot;&gt;my_bool == true&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;shadow=1;" vertex="1" parent="4_OO1GRJjltScIyBnV0f-2">
17+
<mxGeometry x="40" y="200" width="134" height="60" as="geometry" />
18+
</mxCell>
19+
<mxCell id="4_OO1GRJjltScIyBnV0f-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;curved=1;startArrow=classic;startFill=1;endArrow=none;endFill=0;shadow=1;" edge="1" parent="4_OO1GRJjltScIyBnV0f-2" source="4_OO1GRJjltScIyBnV0f-7" target="4_OO1GRJjltScIyBnV0f-4">
20+
<mxGeometry relative="1" as="geometry" />
21+
</mxCell>
22+
<mxCell id="4_OO1GRJjltScIyBnV0f-7" value="&lt;font style=&quot;font-size: 15px;&quot;&gt;&lt;b&gt;AND&lt;/b&gt;&lt;/font&gt;" style="ellipse;whiteSpace=wrap;html=1;aspect=fixed;fillColor=#dae8fc;strokeColor=#6c8ebf;shadow=1;" vertex="1" parent="4_OO1GRJjltScIyBnV0f-2">
23+
<mxGeometry x="290" y="200" width="80" height="80" as="geometry" />
24+
</mxCell>
25+
<mxCell id="4_OO1GRJjltScIyBnV0f-10" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=classic;startFill=1;endArrow=none;endFill=0;edgeStyle=orthogonalEdgeStyle;curved=1;shadow=1;" edge="1" parent="4_OO1GRJjltScIyBnV0f-2" source="4_OO1GRJjltScIyBnV0f-11" target="4_OO1GRJjltScIyBnV0f-7">
26+
<mxGeometry relative="1" as="geometry">
27+
<mxPoint x="328" y="280" as="targetPoint" />
28+
</mxGeometry>
29+
</mxCell>
30+
<mxCell id="4_OO1GRJjltScIyBnV0f-11" value="&lt;span style=&quot;font-size: 15px;&quot;&gt;get_my_int() &amp;gt; 0&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;shadow=1;" vertex="1" parent="4_OO1GRJjltScIyBnV0f-2">
31+
<mxGeometry x="150" y="350" width="140" height="60" as="geometry" />
32+
</mxCell>
33+
<mxCell id="4_OO1GRJjltScIyBnV0f-12" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=classic;startFill=1;endArrow=none;endFill=0;edgeStyle=orthogonalEdgeStyle;curved=1;shadow=1;" edge="1" parent="4_OO1GRJjltScIyBnV0f-2" source="4_OO1GRJjltScIyBnV0f-13" target="4_OO1GRJjltScIyBnV0f-7">
34+
<mxGeometry relative="1" as="geometry">
35+
<mxPoint x="580" y="280" as="targetPoint" />
36+
</mxGeometry>
37+
</mxCell>
38+
<mxCell id="4_OO1GRJjltScIyBnV0f-13" value="&lt;span style=&quot;font-size: 15px;&quot;&gt;my_int &amp;lt; 10&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;shadow=1;" vertex="1" parent="4_OO1GRJjltScIyBnV0f-2">
39+
<mxGeometry x="370" y="350" width="140" height="60" as="geometry" />
40+
</mxCell>
41+
</root>
42+
</mxGraphModel>
43+
</diagram>
44+
</mxfile>
51.3 KB
Loading

include/attwoodn/expression_tree.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,15 +384,15 @@ namespace attwoodn::expression_tree {
384384
* Makes an expression tree leaf node for comparing value-type member variables of a class/struct
385385
*/
386386
template<typename Obj, typename CompValue, typename Op = typename type_id<bool (*)(CompValue, CompValue)>::type>
387-
node::expression_tree_leaf_node<Obj, Op, CompValue>* make_expr( const CompValue Obj::* member_var, Op op, CompValue comp_value ) {
387+
node::expression_tree_leaf_node<Obj, Op, CompValue>* make_expr( const CompValue Obj::* member_var, Op op, const CompValue comp_value ) {
388388
return new node::expression_tree_leaf_node<Obj, Op, CompValue>( member_var, op, comp_value );
389389
}
390390

391391
/**
392392
* Makes an expression tree leaf node for comparing the return value from a class/struct's const member function
393393
*/
394394
template<typename Obj, typename CompValue, typename Op = typename type_id<bool (*)(CompValue, CompValue)>::type>
395-
node::expression_tree_leaf_node<Obj, Op, CompValue>* make_expr( CompValue (Obj::* member_func)() const, Op op, CompValue comp_value ) {
395+
node::expression_tree_leaf_node<Obj, Op, CompValue>* make_expr( CompValue (Obj::* member_func)() const, Op op, const CompValue comp_value ) {
396396
return new node::expression_tree_leaf_node<Obj, Op, CompValue>( member_func, op, comp_value );
397397
}
398398

tests/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ if(BUILD_TESTING)
2020
target_compile_options( expression_tree_test PRIVATE -fsanitize=address )
2121
add_test( expression_tree_test ${EXECUTABLE_OUTPUT_PATH}/expression_tree_test )
2222

23+
add_executable( make_expr_safety_test make_expr_safety.cpp )
24+
target_link_libraries( make_expr_safety_test "-fsanitize=address" )
25+
target_compile_options( make_expr_safety_test PRIVATE -fsanitize=address )
26+
add_test( make_expr_safety_test ${EXECUTABLE_OUTPUT_PATH}/make_expr_safety_test )
27+
2328
endif()

0 commit comments

Comments
 (0)