+
+template< typename T >
+class foo
+{
+public:
+ foo() : value_{} {}
+ foo( T && value ) : value_{ value } {}
+
+ void value( T && value ) { value_ = value; }
+
+ T value() const noexcept { return value_; }
+
+private:
+ T value_{};
+};
+
+int main()
+{
+ foo< std::string > x{ "foo" };
+ foo< std::string > y{ "bar" };
-
+ booleval::evaluator evaluator
+ {
+ booleval::make_field( "field", &foo< std::string >::value )
+ };
-## About
+ if ( !evaluator.expression( "field eq foo" ) )
+ {
+ std::cerr << "Expression not valid!" << std::endl;
+ }
-`booleval` is a small C++17 library for evaluating logical expressions. It implements recursive descent parser mechanism for building an expression tree for a user-defined logical expression. After the expression tree is being built, map of fields and values representing a certain object can be passed to the `evaluator` component of this library which will evaluate those values to `true` or `false` according to the user-defined logical expression.
+ if ( evaluator.is_activated() )
+ {
+ std::cout << std::boolalpha << evaluator.evaluate( x ) << std::endl; // true
+ std::cout << std::boolalpha << evaluator.evaluate( y ) << std::endl; // false
+ }
+
+ return 0;
+}
+```
+
+## Table of Contents
+
+* [Motivation](#motivation)
+* [Getting Started](#getting-started)
+ * [Zero Copy](#zero-copy)
+ * [EQUAL TO Operator](#equal-to-operator)
+ * [Valid Expressions](#valid-expressions)
+ * [Invalid Expressions](#invalid-expressions)
+ * [Evaluation Result](#evaluation-result)
+ * [Supported Tokens](#supported-tokens)
+* [Benchmark](#benchmark)
+* [Compilation](#compilation)
+* [Tests](#tests)
+* [Compiler Compatibility](#compiler-compatibility)
+* [Contributing](#contributing)
+* [License](#license)
+* [Support](#support)
+
+## Motivation
+
+`booleval` is a header-only C++17 library for evaluating logical expressions. It implements recursive descent parser mechanism for building an expression tree for a user-defined logical expression. After the expression tree is being built, map of fields and values representing a certain object can be passed to the `evaluator` component of this library which will evaluate those values to `true` or `false` according to the user-defined logical expression.
@@ -38,25 +91,21 @@ The library is under development and subject to change. Contributions are welcom
-
-
-## Motivation
-
In programming languages like Java and C# accessing arbitrary fields inside a class represents an omnipresent problem. However, it got solved by introducing a **reflections** feature. This feature provides us information about the class to which a certain object belongs to and, also, the methods of that class which we can invoke at runtime.
Since the reflection feature is missing in C++, `booleval` library is implemented. It checks whether the members of a class to which an object belongs to have certain values. Members of a class are specified in a string format and can be used to form a logical expression.
Providing an end-user a functionality of specifying a logical expression is a common way of filtering out a large amounts of objects. E.g. [`tcpdump`](https://www.tcpdump.org/manpages/tcpdump.1.html) and [BPF](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter) (Berkeley Packet Filter), network tools available on most UNIX-like operating systems, have pretty much the same syntax for their filter expression.
-
-
-
-
-
+## Getting Started
-
+`booleval` is a header-only C++17 library for evaluating logical expressions.
-## Specification
+### Zero Copy
+
+In order to improve performance, `booleval` library does not copy objects that are being evaluated.
+
+### EQUAL TO operator
EQUAL TO operator is an optional operator. Therefore, logical expression that checks whether a field with the name `field_a` has a value of `foo` can be constructed in a two different ways:
@@ -65,14 +114,31 @@ EQUAL TO operator is an optional operator. Therefore, logical expression that ch
To conclude, equality operator is a default operator between two fields. Thus, it **does not need** to be specified in the logical expression.
-### Examples of valid expressions
+### Valid expressions
+
- `(field_a foo and field_b bar) or field_a bar`
- `(field_a eq foo and field_b eq bar) or field_a eq bar`
-### Examples of invalid expressions
+### Invalid expressions
+
- `(field_a foo and field_b bar` _Note: Missing closing parentheses_
- `field_a foo bar` _Note: Two field values in a row_
+### Evaluation Result
+
+Result of evaluation process contains two information:
+
+- `success`: `true` if evaluation process is successful; otherwise, `false`
+- `message`: meaningful message if evaluation process is unsuccessful; otherwise, empty message
+
+This kind of enriched lightweight result is being used instead of exceptions for performance reasons.
+
+Examples of messages:
+
+- `"Missing operand"`
+- `"Unknown field"`
+- `"Unknown token type"`
+
### Supported tokens
|Name|Keyword|Symbol|
@@ -88,19 +154,16 @@ To conclude, equality operator is a default operator between two fields. Thus, i
|LEFT parentheses|∅|(|
|RIGHT parentheses|∅|)|
-
-
-## Requirements
+## Benchmark
-`booleval` project requires C++17 compiler and has been tested on:
+Following table shows benchmark results:
-- gcc 8.4.0
-- clang 7.0.0
-- msvc 19.16
-
-There are no 3rd party dependencies.
+|Benchmark|Time|Iterations
+|:---:|:---:|:---:|
+|Building expression tree|3817 ns|180904|
+|Evaluation|1285 ns|532522|
-
+In other words, it is possible to evaluate **2,413,045.84 objects per second**.
## Compilation
@@ -118,8 +181,6 @@ $ # compile
$ make
```
-
-
## Tests
In order to run unit tests, run the following commands:
@@ -142,71 +203,28 @@ $ # run tests
$ make test
```
-If you find that any tests **fail**, please create a ticket in the
-issue tracker indicating the following information:
-- platform
-- architecture
-- library version
-- minimal reproducible example
+## Compiler Compatibility
-
+* Clang/LLVM >= 7
+* MSVC++ >= 19.16
+* GCC >= 8.4
-## Example
+There are no 3rd party dependencies.
-Let's say we have a large number of objects coming through our interface. Objects can be of the following `class obj` type:
+## Contributing
-```c++
-struct obj {
- std::string field_a_;
- uint32_t field_b_;
+Feel free to contribute.
-public:
- obj(std::string const& field_a, uint32_t const field_b)
- : field_a_(field_a),
- field_b_(field_b)
- {}
+If you find that any of the tests **fail**, please create a ticket in the issue tracker indicating the following information:
- std::string const& field_a() {
- return field_a_;
- }
+* platform
+* architecture
+* library version
+* minimal reproducible example
- uint32_t field_b() {
- return field_b_;
- }
-};
-```
+## License
-In our application, we want to let end-users to specify some sort of a rule which will filter out only those objects that contain certain field values. This rule can have a form of a following logical expression `field_a foo and field_b 123`. Now, we can use `booleval::evaluator` component to check whether objects conform specified rule.
-
-```c++
-#include
-#include
-#include
-
-int main() {
- obj pass("foo", 123);
- obj fail("bar", 456);
-
- booleval::evaluator evaluator({
- { "field_a", &obj::field_a },
- { "field_b", &obj::field_b }
- });
-
- auto valid = evaluator.expression("field_a foo and field_b 123");
- if (!valid) {
- std::cerr << "Expression not valid!" << std::endl;
- }
-
- if (evaluator.is_activated()) {
- std::cout << std::boolalpha << evaluator.evaluate(pass) << std::endl; // output: true
- std::cout << std::boolalpha << evaluator.evaluate(fail) << std::endl; // output: false
- } else {
- std::cerr << "Evaluator is not activated!" << std::endl;
- }
-
- return 0;
-}
-```
+The project is available under the [MIT](https://opensource.org/licenses/MIT) license.
## Support
diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt
new file mode 100644
index 0000000..161191f
--- /dev/null
+++ b/benchmark/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required (VERSION 3.8)
+
+set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src)
+add_subdirectory (src)
\ No newline at end of file
diff --git a/benchmark/src/CMakeLists.txt b/benchmark/src/CMakeLists.txt
new file mode 100644
index 0000000..8f924c6
--- /dev/null
+++ b/benchmark/src/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required (VERSION 3.8)
+
+# Use bitflags's include directories + Google Benchmark include directories
+include_directories (
+ ${PROJECT_SOURCE_DIR}/include/
+ ${GOOGLEBENCH_INCLUDE}
+)
+
+link_directories (
+ ${GOOGLEBENCH_LIBRARY}
+)
+
+# Link against GoogleBenchmark and bitflags
+link_libraries (benchmark)
+
+add_custom_target (benchmarks)
+
+macro (create_benchmark benchmark_name)
+ string (REPLACE "/" "_" binary_name ${benchmark_name})
+ set (binary_name "${binary_name}_benchmark")
+ add_executable (${binary_name} EXCLUDE_FROM_ALL "${benchmark_name}_benchmark.cpp")
+ add_test (${benchmark_name} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${binary_name})
+ add_dependencies (benchmarks ${binary_name})
+endmacro ()
+
+# Benchmarks
+
+create_benchmark (booleval)
\ No newline at end of file
diff --git a/benchmark/src/booleval_benchmark.cpp b/benchmark/src/booleval_benchmark.cpp
new file mode 100644
index 0000000..0c5e21d
--- /dev/null
+++ b/benchmark/src/booleval_benchmark.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2020, Marin Peko
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+#include
+#include
+
+namespace
+{
+
+ template< typename T, typename U >
+ class bar
+ {
+ public:
+ bar( T && value_1, U && value_2 )
+ : value_1_{ value_1 }
+ , value_2_{ value_2 }
+ {}
+
+ void value_1( T && value ) noexcept { value_1_ = value; }
+ void value_2( U && value ) noexcept { value_2_ = value; }
+
+ T value_1() const noexcept { return value_1_; }
+ U value_2() const noexcept { return value_2_; }
+
+ private:
+ T value_1_{};
+ U value_2_{};
+ };
+
+} // namespace
+
+void BuildingExpressionTree( benchmark::State & state )
+{
+ booleval::evaluator evaluator
+ {
+ {
+ booleval::make_field( "field_1", &bar< std::string, unsigned >::value_1 ),
+ booleval::make_field( "field_2", &bar< std::string, unsigned >::value_2 )
+ }
+ };
+
+ for (auto _ : state)
+ {
+ [[ maybe_unused ]] auto const success{ evaluator.expression( "(field_1 foo and field_2 1) or (field_1 qux and field_2 2)" ) };
+ benchmark::DoNotOptimize( evaluator );
+ }
+}
+
+BENCHMARK( BuildingExpressionTree );
+
+void Evaluation( benchmark::State & state )
+{
+ booleval::evaluator evaluator
+ {
+ {
+ booleval::make_field( "field_1", &bar< std::string, unsigned >::value_1 ),
+ booleval::make_field( "field_2", &bar< std::string, unsigned >::value_2 )
+ }
+ };
+
+ bar< std::string, unsigned > x{ "foo", 1 };
+
+ [[ maybe_unused ]] auto const success{ evaluator.expression( "(field_1 foo and field_2 1) or (field_1 qux and field_2 2)" ) };
+
+ for (auto _ : state)
+ {
+ [[ maybe_unused ]] auto const result{ evaluator.evaluate( x ) };
+ benchmark::DoNotOptimize( evaluator );
+ benchmark::DoNotOptimize( x );
+ }
+}
+
+BENCHMARK( Evaluation );
+
+BENCHMARK_MAIN();
diff --git a/docs/command-line.gif b/docs/command-line.gif
deleted file mode 100644
index cdc3151..0000000
Binary files a/docs/command-line.gif and /dev/null differ
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index fa19da0..c63d167 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -5,14 +5,9 @@ include_directories (
${CMAKE_CURRENT_SOURCE_DIR}/../include
)
-link_libraries (booleval)
-
add_custom_target (
examples DEPENDS
evaluator
)
-# Make sure we first build libbooleval
-add_dependencies (examples booleval)
-
add_executable (evaluator EXCLUDE_FROM_ALL evaluator.cpp)
\ No newline at end of file
diff --git a/examples/evaluator.cpp b/examples/evaluator.cpp
index 727b0fc..1617cc1 100644
--- a/examples/evaluator.cpp
+++ b/examples/evaluator.cpp
@@ -31,44 +31,51 @@
#include
#include
-struct obj {
+template< typename T, typename U >
+class bar
+{
public:
- obj(std::string const& field_a, uint32_t const field_b)
- : field_a_(field_a),
- field_b_(field_b)
+ bar( T && value_1, U && value_2 )
+ : value_1_{ value_1 }
+ , value_2_{ value_2 }
{}
- std::string const& field_a() const noexcept {
- return field_a_;
- }
+ void value_1( T && value ) noexcept { value_1_ = value; }
+ void value_2( U && value ) noexcept { value_2_ = value; }
- uint32_t field_b() const noexcept {
- return field_b_;
- }
+ T value_1() const noexcept { return value_1_; }
+ U value_2() const noexcept { return value_2_; }
private:
- std::string field_a_;
- uint32_t field_b_;
+ T value_1_{};
+ U value_2_{};
};
-int main() {
- obj pass("foo", 123);
- obj fail("bar", 456);
+int main()
+{
+ bar< std::string, unsigned > x{ "foo", 1 };
+ bar< std::string, unsigned > y{ "bar", 2 };
- booleval::evaluator evaluator({
- { "field_a", &obj::field_a },
- { "field_b", &obj::field_b }
- });
+ booleval::evaluator evaluator
+ {
+ {
+ booleval::make_field( "field_1", &bar< std::string, unsigned >::value_1 ),
+ booleval::make_field( "field_2", &bar< std::string, unsigned >::value_2 )
+ }
+ };
- auto valid = evaluator.expression("field_a foo and field_b 123");
- if (!valid) {
+ if ( !evaluator.expression( "(field_1 foo and field_2 1) or (field_1 qux and field_2 2)" ) )
+ {
std::cerr << "Expression not valid!" << std::endl;
}
- if (evaluator.is_activated()) {
- std::cout << std::boolalpha << evaluator.evaluate(pass) << std::endl;
- std::cout << std::boolalpha << evaluator.evaluate(fail) << std::endl;
- } else {
+ if ( evaluator.is_activated() )
+ {
+ std::cout << std::boolalpha << evaluator.evaluate( x ).success << std::endl;
+ std::cout << std::boolalpha << evaluator.evaluate( y ).success << std::endl;
+ }
+ else
+ {
std::cerr << "Evaluator is not activated!" << std::endl;
}
diff --git a/googlebench b/googlebench
new file mode 160000
index 0000000..e451e50
--- /dev/null
+++ b/googlebench
@@ -0,0 +1 @@
+Subproject commit e451e50e9b8af453f076dec10bd6890847f1624e
diff --git a/include/booleval/evaluator.hpp b/include/booleval/evaluator.hpp
index b042eb4..2c27bba 100644
--- a/include/booleval/evaluator.hpp
+++ b/include/booleval/evaluator.hpp
@@ -30,13 +30,17 @@
#ifndef BOOLEVAL_EVALUATOR_HPP
#define BOOLEVAL_EVALUATOR_HPP
-#include