Skip to content

Commit

Permalink
Merge pull request #6 from tirimatangi/fix-equality-operator
Browse files Browse the repository at this point in the history
Fix equality operator
  • Loading branch information
tirimatangi authored Sep 12, 2022
2 parents d95f5b4 + fcb23bb commit 2224260
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 105 deletions.
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,20 +250,52 @@ expr evaluated with offset 20: { { 22.2 24.8 30.4 } { 21.05 20.55 20.2875 } }
For runnable code, see example 4 in [tutorial-example.cc](https://github.com/tirimatangi/LazyExpression/blob/main/examples/tutorial-example.cc).
## Compare an expression with another expression or a container
Entire expressions can be compared with other compatible expressions or containers.
By compatible we mean that the result types must be the same.
In this example we show how the function and arguments can be extracted from an expression with
member functions `funcRef()` and `argsRef()` and use them to define new expressions
which we then compare to the original expression.
- `expr.funcRef()` returns a reference to the function of the expression.
- `expr.argsRef()` returns a reference to a tuple which contains the argument containers.
```c++
vector<vector<int>> matX { {0, 1, 2, 3}, {4, 5, 6, 7} },
matY { {10, 11, 12, 13}, {14, 15, 16, 17} };
auto expr = Expression{[](int x, int y){ return 2.0 * x + y; }, matX, matY};
// Define new expressions reusing the function and the arguments of an old expression.
auto exprA = Expression{expr.funcRef(), std::get<0>(expr.argsRef()), std::get<1>(expr.argsRef())};
auto exprB = Expression{expr.funcRef(), std::get<1>(expr.argsRef()), std::get<0>(expr.argsRef())};
assert(expr == exprA); // Compares two expressions
assert(expr != exprB()); // Compares an expression and a container
// exprA == { { 10 13 16 19 } { 22 25 28 31 } }
// exprB == { { 20 23 26 29 } { 32 35 38 41 } }
```

Now expressions `expr` and `exprA` produce the same output whereas `exprB` produces a different
output because the input arguments are reversed.

For a runnable example, see example 9 in [tutorial-example.cc](https://github.com/tirimatangi/LazyExpression/blob/main/examples/tutorial-example.cc).

## Copies or references?

Expression object captures both the function (i.e. the callable object) and all argument containers by default as copies, just like the capture list of C++ lambda expressions does. This helps avoid dangling references which can be very difficult to debug. On the other hand it may make plenty of unnecessary copies.

If you know that the lifetimes of the function or (some of) the argument containers are longer than the lifetime of the expression, you can pass them as references with `std::ref()`. Actually we could have used references in all the above examples.

Alternatively you can use convenience function `makeRefExpression` which makes an expression assuming that all arguments can be safely captured by reference.
Alternatively you can use convenience function `makeRefExpression` which makes an expression assuming that all arguments can be safely captured by reference. Use references whenever you can. Expressions with references are much faster than the ones which make copies.

So in the previous example we defined
```c++
//...
auto expr = Expression{f, c1, c2};
```
whereas we could have said
whereas we could (and indeed should) have said
```c++
auto expr = Expression{std::ref(f), std:ref(c1), std::ref(c2)};
```
Expand Down Expand Up @@ -443,5 +475,4 @@ The easiest way to compile all examples is to do
If you don't want to use cmake, the examples can be compiled manually one by one. For instance, <br>
`g++ examples/tutorial-example.cc -std=c++17 -I include/ -O3 -o tutorial-example`

The examples have been tested with g++ 10.3.0 and clang++ 12.0.0 but any compiler which complies with c++17 standard should do.

The examples have been tested with g++ 11.2.0 and clang++ 14.0.0 but any compiler which complies with c++17 standard should do.
91 changes: 56 additions & 35 deletions examples/tutorial-example.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,11 @@ using LazyExpression::Expression;
using LazyExpression::makeRefExpression;
using LazyExpression::asExpression;

// Container printers.
template<typename T, template <class...> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T>& vec);
template<typename T, auto N, template <class, size_t> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T, N>& vec);
std::ostream& operator<<(std::ostream& os, std::string str);
// Print a complex number.
template<typename T>
std::ostream& operator<<(std::ostream& os, std::complex<T> c);

// Print a vector- or list-like container.
template<typename T, template <class...> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T>& vec)
{
os << "{ ";
for (auto& el : vec)
{
os << el << ' ';
}
os << "} ";
return os;
}

// Print an array-like container.
template<typename T, auto N, template <class, size_t> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T, N>& vec)
std::ostream& operator<<(std::ostream& os, std::complex<T> c)
{
os << "{ ";
for (auto& el : vec)
{
os << el << ' ';
}
os << "} ";
os << c.real() << ((c.imag() >= 0) ? '+' : '-') << std::abs(c.imag()) << 'i';
return os;
}

Expand All @@ -73,11 +46,24 @@ std::ostream& operator<<(std::ostream& os, std::string str)
return os;
}

// Print a complex number.
template<typename T>
std::ostream& operator<<(std::ostream& os, std::complex<T> c)
// Print a container or an expression.
template <class C, class = decltype(std::declval<C>().begin())>
std::ostream& operator<<(std::ostream& os, const C& container)
{
os << c.real() << ((c.imag() >= 0) ? '+' : '-') << std::abs(c.imag()) << 'i';
// Demonstrate how to test if the type is an expression
char lparen, rparen;
if constexpr (LazyExpression::IsExpression<C>()) {
lparen = '{'; rparen = '}';
} else {
lparen = '['; rparen = ']';
}

os << lparen << ' ';
for (const auto& element : container)
{
os << element << ' ';
}
os << rparen << ' ';
return os;
}

Expand Down Expand Up @@ -361,5 +347,40 @@ int main()
cout << "Mul: " << a << " * " << b << " = " << (exprA * exprB)() << "\n";
cout << "Div: " << a << " / " << b << " = " << (exprA / exprB)() << "\n";
}

std::cout << "\n*** Example 9 *** : Compare expressions \n";
{
// 2 x 4 matrices of X- and Y-coordinates.
vector<vector<int>> matX { {0, 1, 2, 3}, {4, 5, 6, 7} },
matY { {10, 11, 12, 13}, {14, 15, 16, 17} };

auto squaredDistance = [](int x, int y){ return double(x)*x + double(y)*y;};
// Expression for squared distance from the origin as double.
// The expression behaves as if it was a vector<vector<double>>.
auto expr1 = Expression{squaredDistance, matX, matY};

// Define the same expression using expression arithmetics.
auto asDouble = [](int x) { return double(x); }; // Convert int -> double
auto exprX = Expression{asDouble, ref(matX)}; // Use ref to avoid copies
auto exprY = Expression{asDouble, ref(matY)};
auto expr2 = exprX * exprX + exprY * exprY;

// Verify the similarity of every entry.
assert(expr1 == expr2);

// Evaluate the whole expressions into nested containers.
vector<vector<double>> container1 = expr1(),
container2 = expr2();
// Verify again as containers.
assert(container1 == container2);

// Make a new expression which re-uses the function
// and argument containers of an existing expression.
auto exprRecycled = Expression{expr1.funcRef(), std::get<0>(expr1.argsRef()), std::get<1>(expr1.argsRef())};
// As always, expect the identical result.
// Note that you can compare an expression with a container of the same dimension.
assert(exprRecycled == container1);
cout << "Squared distances: " << exprRecycled << "\n";
}
return 0;
}
}
144 changes: 111 additions & 33 deletions examples/unit-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct whatType

using std::array;
using std::vector;
using std::list;
using std::deque;
using std::cout;
using std::size_t;
Expand Down Expand Up @@ -72,50 +73,39 @@ std::ostream& operator<<(std::ostream& os, std::string str);

using namespace LazyExpression;

// Container printers.
template<typename T, template <class...> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T>& vec);
template<typename T, auto N, template <class, size_t> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T, N>& vec);
std::ostream& operator<<(std::ostream& os, std::string str);
// Print a complex number.
template<typename T>
std::ostream& operator<<(std::ostream& os, std::complex<T> c);

template<typename T, template <class...> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T>& vec)
{
os << "{ ";
for (auto& el : vec)
{
os << el << ' ';
}
os << "} ";
return os;
}

template<typename T, auto N, template <class, size_t> class Container>
std::ostream& operator<<(std::ostream& os, const Container<T, N>& vec)
std::ostream& operator<<(std::ostream& os, std::complex<T> c)
{
os << "{ ";
for (auto& el : vec)
{
os << el << ' ';
}
os << "} ";
os << c.real() << ((c.imag() >= 0) ? '+' : '-') << std::abs(c.imag()) << 'i';
return os;
}

// Print a string.
std::ostream& operator<<(std::ostream& os, std::string str)
{
os << "\'" << str.c_str() << "\'";
return os;
}

// Print a complex number.
template<typename T>
std::ostream& operator<<(std::ostream& os, std::complex<T> c)
// Print a container or an expression.
template <class C, class = decltype(std::declval<C>().begin())>
std::ostream& operator<<(std::ostream& os, const C& container)
{
os << c.real() << ((c.imag() >= 0) ? '+' : '-') << std::abs(c.imag()) << 'i';
// Demonstrate how to test if the type is an expression.
char lparen, rparen;
if constexpr (LazyExpression::IsExpression<C>()) {
lparen = '{'; rparen = '}';
} else {
lparen = '['; rparen = ']';
}

os << lparen << ' ';
for (const auto& element : container)
{
os << element << ' ';
}
os << rparen << ' ';
return os;
}

Expand Down Expand Up @@ -871,7 +861,7 @@ void test_b()
else
cout << "e0 != eb !!! \n\n";

cout << "Doing e00 = e0[0]...\n"; //TÄMÄ Ei TOIMI
cout << "Doing e00 = e0[0]...\n";
auto e00 = e0[0];
cout << "Done e00 = e0[0]...\n";
cout << "e00 = " << e00 << "\n";
Expand Down Expand Up @@ -1095,6 +1085,90 @@ void test_d()
}
}

void test_e() // Test equality operator
{
using T = int;
using VLA = vector<list<array<T, 3>>>;
VLA vla1 { { { 2,3,1 }, { 5,6,4 }, { 8,9,7 } },
{ { 12,13,11 }, { 15,16,14 }, { 18,19,17 } } };
auto expr = Expression{[](T x){ return x; }, vla1};
auto expr1 = asExpression(vla1);
auto expr2 = expr * expr;
auto expr3 = Expression{[](T x){return x*x;}, vla1};
auto expr4 = Expression{[&vla1](int){return vla1;}, 0}; // dimension() == 0
auto expr5 = Expression{[](const array<T,3>& a){return a;}, vla1};
auto expr6 = Expression{[](const list<array<T,3>>& la){return la;}, vla1};
auto exprD = Expression{[](T x){ return double(x); }, expr};
vector<int> bA, bB, bC, bD;
bA.push_back(expr == expr);
bA.push_back(expr == expr1);
bA.push_back(expr == expr2);
bA.push_back(expr == expr3);
bA.push_back(expr == expr4);
bA.push_back(expr == expr5);
bA.push_back(expr == expr6);
bA.push_back(expr2 == expr3);

bB.push_back(expr == expr());
bB.push_back(expr == expr1());
bB.push_back(expr == expr2());
bB.push_back(expr == expr3());
bB.push_back(expr == expr4());
bB.push_back(expr == expr5());
bB.push_back(expr == expr6());
bB.push_back(expr2() == expr3);

bC.push_back(expr != expr);
bC.push_back(expr != expr1);
bC.push_back(expr != expr2);
bC.push_back(expr != expr3);
bC.push_back(expr != expr4);
bC.push_back(expr != expr5);
bC.push_back(expr != expr6);
bC.push_back(expr2 != expr3);

bD.push_back(expr != expr());
bD.push_back(expr != expr1());
bD.push_back(expr != expr2());
bD.push_back(expr != expr3());
bD.push_back(expr != expr4());
bD.push_back(expr != expr5());
bD.push_back(expr != expr6());
bD.push_back(expr2() != expr3);

cout << " A: " << bA << "\n";
cout << " B: " << bB << "\n";
cout << "!A: " << bC << "\n";
cout << "!B: " << bD << "\n";

assert((Expression{[](int i) { return int(!i); }, bA} == bC));

// Equality of zero-dimensional expression
auto exprZero = Expression{[](int i){ return i;}, 99};
bool zA = (exprZero == 99);
bool zB = (exprZero() == 99);
cout << "Zero: {" << zA << ',' << zB << "}\n";
assert(zA && zB);
}

void test_f()
{
vector<vector<int>> matX { {0, 1, 2, 3}, {4, 5, 6, 7} },
matY { {10, 11, 12, 13}, {14, 15, 16, 17} };

double z = 1000;
auto squaredDistance = [&z](int x, int y){ return double(x)*x + double(y)*y + z;};
// Expression for squared distance from the origin as double.
// The expression behaves as if it was a vector<vector<double>>.
auto ex = Expression{squaredDistance, matX, matY};

whatType<decltype(ex.funcRef())>("func type: ")();
whatType<decltype(ex.argsRef())>("args types:")();

assert(ex.funcRef()(1,2) == squaredDistance(1,2));
assert(std::get<0>(ex.argsRef()) == matX);
assert(std::get<1>(ex.argsRef()) == matY);
}

int main()
{
Expand All @@ -1106,5 +1180,9 @@ int main()
test_c();
cout << "##### doing test_d() #####\n";
test_d();
cout << "##### doing test_e() #####\n";
test_e();
cout << "##### doing test_f() #####\n";
test_f();
cout << "##### DONE #####\n";
}
Loading

0 comments on commit 2224260

Please sign in to comment.