Defining new infix operators
Asked Answered
S

1

28

So thanks to C++11, it's now possible to combine macros, user-defined literals, lambdas, etc. to create the closest I can get to 'syntactic sugar'. An example would be

 if (A contains B)

Of course this is easy.

cout <<("hello"_s contains "ello"_s)<<endl;

The expression converts to a bool, where contains is a custom struct that takes the left-hand side and right-hand side as arguments. The struct of course overloads operator+ to take the custom string literal first, returning itself, then the operator+ for the struct itself.

struct contains_struct {
    string lhs;
    string rhs;
    void set_lhs(string lhs) { this->lhs = lhs; }
    void set_rhs(string rhs) { this->rhs = rhs; }
    operator bool() const {
        return string::npos != lhs.find(rhs);
    }
} contains_obj;

contains_struct& operator+(const string& lhs, const contains_struct& rhs) {
    contains_obj.set_lhs(lhs);
    return contains_obj;
}

contains_struct& operator+(const contains_struct& lhs, const string& rhs) {
    contains_obj.set_rhs(rhs);
    return contains_obj;
}

#define contains +contains_obj+

Now I decided I want to go further. What about

(x in a) perform cube

It's no list comprehension, but it's a pretty good example right? At first I said, well I'd have to go to stackoverflow to ask about custom operator precedence, but it's straight forward to put it in parentheses since no one in their right mind would use my code. Instead, I expanded upon my other example and have 'in' and 'perform' as custom structs, just like 'contains'.

You can go further and template it so that x can be any numerical index, and a as any container, but for simplicity, I left x as an integer and a as a vector of ints. Now so far it doesn't actually take the local variable x as an argument, it uses it locally in the operator string() function.

To simplify things, I store the results of the expression in a string, like so

operator string() const {
    string s = "";
    for (int x : lhs.rhs)
        s += to_string(rhs(x)) + string("\n");
    return s;
}

Thanks to another question: Overloading assignment operator for type deduction

I realized one practical use for returning it as an assignment is the following:

struct result_struct {
    vector<int> results;
    result_struct(vector<int> results) { this->results = results; }
};

...

    operator result_struct() const {
        vector<int> tmp;
        for (int x : lhs.rhs)
            tmp.push_back(rhs(x));
        return result_struct(tmp);
    }

...

result_struct result_2 = (x in a) perform cube;
    for (int x : result_2.results)
        cout <<x<<endl;

Thanks to milleniumbug's answer, I can do:

struct for_obj
{
    int _lhs;
    std::vector<int> _rhs;
    for_obj(int lhs, std::vector<int> rhs)
        : _lhs(lhs), _rhs(rhs) { }
};

INFIX_OPERATOR(for_obj, in_op, int, std::vector<int>)
{
    return for_obj(lhs(), rhs());
}
#define in + in_op() +

INFIX_OPERATOR(int, perform_op, for_obj, std::function<int(int)>)
{
    for (int i = 0; i < lhs()._rhs.size(); i++)
        rhs()(lhs()._rhs[i]);
    return 0;
}
#define perform + perform_op() +

There are two caveats. First, I return an int so that I can assign it to a dummy variable to get it to execute. I could always do the result_struct thing I did before, or return a std::function object to call it by itself, but I'd be repeating myself. The other caveat is that because there are so many consts in the macro, you cannot modify the lhs (which doesn't allow you to specifiy an iterator).

All things considered, the following works as expected.

int x = 0;
std::vector<int> nums = { 1, 2, 3 };
auto cube = [] (int x)
{
    std::cout << x * x * x << std::endl;
    return x * x * x;  
};
int i = (x in nums) perform cube;

New version

class PerformObj {
    int counter;
public:
    PerformObj() : counter(0) { }
    ~PerformObj() { }
    InObj lhs;
    std::function<int(int)> rhs;

    operator int() const {
        return rhs(lhs.rhs[counter]);
    }
} performobj;

#define perform + performobj +

PerformObj& operator+(const InObj& lhs, PerformObj& rhs) {
    rhs.lhs = lhs;
    return rhs;
}

PerformObj& operator+(PerformObj& lhs, const std::function<int(int)>& rhs) {
    lhs.rhs = rhs;
    return lhs;
} 

int main()
{
    std::vector<int> nums = {1,2,3};
    int x = 0;

    auto cube = [] (int n) {
        return n * n * n;
    };

    std::cout << x in nums perform cube << std::endl;
}

explicit operator std::vector<int>() const {
    std::vector<int> temp;
    for (int i = 0; i < lhs.rhs.size(); i++) {
        temp.push_back(rhs(lhs.rhs[i]));
    }
    return temp;
}

int y = 0;
std::cout << y in static_cast<std::vector<int>>(x in nums perform cube) perform std::function<int(int)>([] (int i) -> int {
        return i;
}) << std::endl;

Should I make it so that instead of infix operators, there are postfix operators, like "String literal"s.contains "Other string literal"s, or do it function style, "String literal"s.contains("Other string literal"s)?

How would I improve my code to make it more extensible? As it is right now, it's very polluted. Is there a better/more generalized/less clunky way to do this? For example, to generalize the expressions so that I don't need define statements or to reuse code.

Scratchboard answered 26/3, 2013 at 8:10 Comment(5)
This is a good technique for obfuscation, but should definitly be avoided otherwise. It makes it impossible for someone who knows C++ to read your code.Mojgan
You seem to be reinventing expression templates… look up the term and you should find plenty. But as for introducing new infix operators to the language, bad idea. Stick with function call notation or lose popularity.Meredeth
I'm upvoting this because it's interesting and neat, but I definitely do not advocate it for actual usage in real code.Dishwater
horrible hack this. if you want a scripting language, use one.Kobold
For what it's worth, this has been possible since before C++11. See: #1515899Coyle
I
14

It is hard to see what is the question asked here, assuming the latest edit has all the questions.

Should I make it so that instead of infix operators, there are postfix operators, like "String literal"s.contains "Other string literal"s, or do it function style, "String literal"s.contains("Other string literal"s)?

Yes. "String literal"s.contains("Other string literal"s) is the best way - concise, clear to C++ programmers, clear to programmers of other languages (Java and Python strings have methods) and no template magic nor macro magic is used.

How would I improve my code to make it more extensible? As it is right now, it's very polluted. Is there a better/more generalized/less clunky way to do this? For example, to generalize the expressions so that I don't need define statements or to reuse code.

Yep! But only to certain degree (removed the unnecessary consts over there and here):

#define INFIX_OPERATOR(rettype, name, LT, RT) \
struct name\
{\
private:\
    LT* left;\
    RT* right;\
\
protected:\
    LT& lhs() const { return *left; }\
    RT& rhs() const { return *right; }\
\
public: \
    friend name operator+(LT& lhs, name && op)\
    {\
        op.left = &lhs;\
        return op;\
    }\
\
    friend name operator+(name && op, RT& rhs)\
    {\
        op.right = &rhs;\
        return op;\
    }\
\
    name () : left(nullptr), right(nullptr) {}\
\
    operator rettype() const;\
};\
\
inline name :: operator rettype() const

And then you can create your infix operator like this:

#include <iostream>
#include <string>

INFIX_OPERATOR(bool, contains_op, const std::string, const std::string)
{
    return std::string::npos != lhs().find(rhs());
}
#define contains + contains_op() +

int main()
{
    std::string a = "hello";
    std::string b = "hell";
    if(a contains b)
        std::cout << "YES";
}

Note that there is no way to avoid #define contains directive, as there is no way to create macro directive with another macro directive.

What are the practical benefits of this if there are any (ignoring all rationality of using this as real world code. I mean what can you get out of it for what I'm using it for, barring recreational purposes?) Say that my friend, instead of learning C++, wants an easy abstracted interface for his Bash or Perl experience but would like to collaborate without resorting to compiling/linking outside gcc. That way, he can write 'scripts' or 'code' that is C++, and compile and link it with my programs/libraries/interface, whatever.

It seems that you are trying to create a language on top of another language. Prepare for

  • Hours and hours trying to test your language.
  • Embarrassingly bad diagnostics messages. Try to compile this: std::vector<void> myarr;1 Then wrap it with macros. And then wrap it in another template. And then in another macros... You get the idea.
  • Debugging tools showing processed code.
  • Even if your language perfectly integrates with itself, you still have C++ to take care of, with tons of rules and complicated type system. After all, all abstractions are leaky.

If your friend want to program in Perl, just let him do it. These languages are easy to interface with C.

If you're trying to create a language, because the other languages can't cleanly express what you're trying to do, parser generators (Flex/Bison, ANTLR) and LLVM make it easy.

If creating a parser is overkill, take a look at D language mixins. They accept a string created at compile time, and then compile it as if it was inserted directly.

Here...

import std.stdio;
int main()
{
    mixin(`write("Hello world");`); //`contents` is a raw string literal
    return 0;                       //so is r"contents"
}

is equivalent to:

import std.stdio;
int main()
{
    write("Hello world");
    return 0;
}

This is just a simple example. You could have your function that parses a string:

mixin(user1508519s_language(r"(x in a) perform cube"));

1 - Here is how it looks (gcc 4.7.2):

In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/bits/stl_construct.h:63:0,
                 from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:63,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/ext/alloc_traits.h
: In instantiation of 'struct __gnu_cxx::__alloc_traits<std::allocator<void> >':

c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
76:28:   required from 'struct std::_Vector_base<void, std::allocator<void> >'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
208:11:   required from 'class std::vector<void>'
#templateerrors2.cpp:5:19:   required from here
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/ext/alloc_traits.h
:189:53: error: no type named 'reference' in 'class std::allocator<void>'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/ext/alloc_traits.h
:190:53: error: no type named 'const_reference' in 'class std::allocator<void>'
In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:65:0,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
 In instantiation of 'class std::vector<void>':
#templateerrors2.cpp:5:19:   required from here
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
292:7: error: forming reference to void
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
467:7: error: forming reference to void
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
684:7: error: invalid parameter type 'std::vector<void>::value_type {aka void}'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
684:7: error: in declaration 'void std::vector<_Tp, _Alloc>::resize(std::vector<
_Tp, _Alloc>::size_type, std::vector<_Tp, _Alloc>::value_type)'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
881:7: error: forming reference to void
In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:70:0,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/vector.tcc:10
8:5: error: forming reference to void
In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:65:0,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
1003:7: error: forming reference to void
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
1179:7: error: forming reference to void
In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:70:0,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/vector.tcc:21
6:5: error: forming reference to void
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/vector.tcc:43
9:5: error: forming reference to void
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/vector.tcc:31
6:5: error: forming reference to void
In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:65:0,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
 In instantiation of 'std::_Vector_base<_Tp, _Alloc>::~_Vector_base() [with _Tp
= void; _Alloc = std::allocator<void>]':
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
247:15:   required from 'std::vector<_Tp, _Alloc>::vector() [with _Tp = void; _A
lloc = std::allocator<void>]'
#templateerrors2.cpp:5:19:   required from here
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
161:9: error: invalid use of 'void'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
 In instantiation of 'void std::_Vector_base<_Tp, _Alloc>::_M_deallocate(std::_V
ector_base<_Tp, _Alloc>::pointer, std::size_t) [with _Tp = void; _Alloc = std::a
llocator<void>; std::_Vector_base<_Tp, _Alloc>::pointer = void*; std::size_t = u
nsigned int]':
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
161:9:   required from 'std::_Vector_base<_Tp, _Alloc>::~_Vector_base() [with _T
p = void; _Alloc = std::allocator<void>]'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
247:15:   required from 'std::vector<_Tp, _Alloc>::vector() [with _Tp = void; _A
lloc = std::allocator<void>]'
#templateerrors2.cpp:5:19:   required from here
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
175:4: error: 'struct std::_Vector_base<void, std::allocator<void> >::_Vector_im
pl' has no member named 'deallocate'
In file included from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/bits/stl_algobase.h:66:0,
                 from c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/
c++/vector:61,
                 from #templateerrors2.cpp:1:
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_iterator_
base_types.h: In instantiation of 'struct std::iterator_traits<void*>':
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_construct
.h:127:24:   required from 'void std::_Destroy(_ForwardIterator, _ForwardIterato
r) [with _ForwardIterator = void*]'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_construct
.h:155:7:   required from 'void std::_Destroy(_ForwardIterator, _ForwardIterator
, std::allocator<_T2>&) [with _ForwardIterator = void*; _Tp = void]'
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_vector.h:
403:9:   required from 'std::vector<_Tp, _Alloc>::~vector() [with _Tp = void; _A
lloc = std::allocator<void>]'
#templateerrors2.cpp:5:19:   required from here
c:\__moje\prog\mingw\bin\../lib/gcc/mingw32/4.7.2/include/c++/bits/stl_iterator_
base_types.h:182:43: error: forming reference to void
Indamine answered 29/3, 2013 at 12:57 Comment(4)
Can we avoid the #define INFIX_OPERATOR though? It seems like that might be possible using templates, at the price of requiring a more explicit definition of the actual operator header.Institutional
Also, would appreciate your comment on my answer to this question.Institutional
@Institutional Yes, you can write the code that macro generates yourself, manually. I'm not necessarily opposed to macro usage as long as: the code they generate is self contained, the macro is properly named with uppercase letters (in which case it only collides with other macros), and the macro name is properly namespaced with project name (in which case it is unlikely to collide with anything)Indamine
What I was wondering is whether we could have the generator as a non-macro, so that we don't have to write it ourselves nor use the macro.Institutional

© 2022 - 2024 — McMap. All rights reserved.