Is there a way to call multiple functions on the same object with one line?
Asked Answered
R

6

20

Just trying to tidy up a program and was wondering if anyone could feed me some syntax sugar with regard to calling a member function on one queue multiple times on the same line.

For example, changing:

queue<int> q;
q.push(0);
q.push(1);

to something like:

q.(push(0), push(1));
//or
q.push(0).push(1);

I know it looks a little ridiculous, and it isn't practical. But if I wanted to shorten a small portion of code like that, is there an option to do so? From what I've read so far, it's only possible to chain methods when the function has a non-void return value.

Of course, this is an option:

q.push(0); q.push(1);

But I'm trying to avoid having q there twice. Again... syntactic sugar :)

The goal here is not to initialize, but to condense the number of times an object/container is brought up in a block of code. The reason I'm referencing a queue is because it's dynamic.

Rawlins answered 3/10, 2016 at 19:7 Comment(8)
make q.push() to return the reference to q.Ariellearies
Use an initializer list?Hardy
If >=C++11: queue<int> q{0,1}Hygienics
This style named Fluent interface. But be very careful with order of execution.Linguiform
Edited the post to clarify that I'm not looking to initialize the queue. Thanks for the link @Linguiform ! ~ Very useful information. Unfortunately, it appears my hands are tied :)Rawlins
You mean like a map function? Maybe this can help: https://mcmap.net/q/662550/-how-to-create-a-map-function-in-cSpinks
I'm actually wondering why we don't have a good canonical for this question (regarding the c++ tag). Well, this one may serve as a duplicate in future.Outwardly
in Pascal and VB you have with keywordScreed
T
23

If you have a class that you can modify, make the function return a reference to itself:

template<typename T>
class queue {
public:
    //...
    queue& push(T data) {
        //...
        return *this; //return current instance
    }
    //...
private:
    //...
};

Then you can do

queue<int> q;
q.push(0).push(1);

If you can't, then your hands are tied. You could make a wrapper around the class, but to save a few characters, this is hardly worth the effort.

In your case with push, you can do:

queue<int> q = { 0, 1 };

But this obviously only works with push, as the queue will contain 0 and 1 after the 2 push's.

Toponym answered 3/10, 2016 at 19:16 Comment(9)
"push(1) can be executed before push(0)" <-- no, it can't.Indian
@Indian But the order of evaluation is unspecified, so it can.Toponym
In q.push(f()).push(g()), the evaluation of f() and g() is unspecified, but q.push(f()) definitely has to be executed before the second call to push().Indian
q.push(0).push(1) is something like push(push(q, 0), 1). Obviously arguments of a function are calculated before it starts to perform.Aurilia
@Indian Jup, sorry. You just found a massive flaw in my understanding of order of evaluation :) Thanks a lot again ;DToponym
@Barry, Here's the proposal for refining evaluation order. Doesn't this mean that the order is still unspecified? (at least prior to C++17)Rawlins
@MattGalaxy: It never was unspecified. It can't be, because the result of the call to the first push is required in order to evaluate the second.Oid
I understand now! So (in this case) order of evaluation is more concerned with evaluating the arguments of the method calls, not with evaluating the sequence of method calls on the object.Rawlins
@MattGalaxy Exactly :)Toponym
G
10

You can always just define a wrapper, like

template< class Item >
void push( queue<Item>& q, std::initializer_list<Item> const& values )
{
    for( Item const& v : values ) { q.push( v ); }
}

Then call it like this:

push( q, {1, 2, 3} );

If what you want is not notational convenience but rather just to use the fluent interface technique, then if you can't modify the class, define an operator:

template< class Item >
auto operator<<( queue<Item>& q, Item v )
    -> queue<Item>&
{ q.push( move( v ) ); return q; }

Then call it like this:

q << 1 << 2 << 3;

Be sure to record your colleague trying to get to grips with the code. :)

Oh, OK, still, if you can't modify the class, you can of course do this:

template< class Item >
struct Fluent
{
    queue<Item>& items;

    auto push( Item v )
        -> Fluent&
    { items.push( move( v ) ); return *this; }

    Fluent( queue<Item>& q ): items( q ) {}
};

Then call it like this:

Fluent( q ).push( 1 ).push( 2 ).push( 3 );

Disclaimer: none of the code touched by compiler.

Have fun!

Goosestep answered 3/10, 2016 at 19:37 Comment(0)
H
3

Just for fun here is a small template trick that provides a way to chain almost every method, ignoring the return values:

// The struct providing operator()(...) so that a call is simply
// chainer_t_instance(param_for_call1)(param_for_call2)(param_for_call3);
template <typename Class, typename Method>
struct chainer_t
{
    chainer_t(Class& instance, Method&& method) :
        _instance(instance),
        _method(method)
    {}

    chainer_t(chainer_t&& chainer) :
        _instance(chainer._instance),
        _method(chainer._method)
    {}

    // Avoid copy to avoid misunderstanding
    chainer_t(const chainer_t&) = delete;    
    chainer_t& operator=(const chainer_t&) = delete;

    // Operator () takes anything
    template <typename... Types>
    chainer_t& operator()(Types&&... types)
    {
        (_instance.*_method)(std::forward<Types>(types)...);
        return *this;
    }

protected:
    Class& _instance;
    Method& _method;
};

// Just to ease the writting
template <typename Class, typename Method>
chainer_t<Class, Method> chain(Class& instance, Method&& method)
{
    using chainer = chainer_t<Class, Method>;
    return chainer(instance, std::forward<Method>(method));
}

A chained call will then just be:

chain(my_instance, &my_class::add)(1)(2)(3)(4);

Live example

Heymann answered 3/10, 2016 at 19:42 Comment(3)
More like function composition if you ask me, chaining needs the this().that().the_other(thing)Sodality
@Sodality Function composition needs the second function called to take the output of the first, so it's not that. But "chaining" is a bit misleading, yeah.Heymann
Oh right, the syntax made me forget we're trying to preserve the objectSodality
H
3
auto repeat_call = [](auto&& f){
  return y_combinate(
    [f=decltype(f)(f)](auto&& self, auto&&...args)->decltype(self){
      f( decltype(args)(args)... );
      return decltype(self)(self);
    }
  );
};

With y_combinate being a y combinator.

Now we can repeat_call( [&](int x){ q.push(x); } )(1)(0);

Hognut answered 3/10, 2016 at 22:29 Comment(0)
F
2

If you can't modify the class, you can still use the comma operator:

#include<queue>
#include<iostream>

int main() {
    std::queue<int> q;
    (q.push(0), q).push(1);
    std::cout << q.size() << std::endl;
}
Fly answered 3/10, 2016 at 22:9 Comment(0)
B
1

This may not be precisely what you were looking for but do not forget, C++ is not a line based language (well except for // comments).

Therefore it is perfectly reasonable to put multiple short, simple statements onto a single line. Thus to achieve:

calling a member function on one queue multiple times on the same line.

You need merely change:

queue<int> q;
q.push(0);
q.push(1);

Into:

queue<int> q;
q.push(0); q.push(1);

No, it does not remove typing q twice, but if that is an issue I would suspect it is more likely your problem is variables with excessively long names. Assuming that is the case always remember you can use references to give simpler local handles to a variable:

auto &foo = a_really_long_name_for_a_queue;
foo.push(0); foo.push(1);
Buncombe answered 3/10, 2016 at 19:56 Comment(3)
Please don't write code like this. It solves zero problems at the expensive of making it both harder to read and potentially introducing bugs if somebody tries to wrap your "one line" in an if statement...Indian
@Indian I very much respect your opinion on this, and indeed I believe it can make code harder to read to have multi-statement lines. However I also believe if used carefully it can actually improve readability though this is a greatly contended point. I personally don't believe in putting too much effort into ensuring people who change code without reading it properly don't cause themselves errors. But again, this is a very opinion based issue and realize many will disagree with me.Buncombe
@vality - I'm sure glad I don't work where you do. I can't imagine having someone on my coding team who doesn't "believe in putting too much effort" into making his/her code readable. Sure, someone modifying your code should read it. But to deliberately incorporate a coding style that increases the likelihood of someone else misreading it is irresponsible. One statement per line is a very common requirement in coding style documents because it enhances readability. In addition, compiler errors and dump traces are generally line oriented, making your code harder to debug as well as harder read.Jeremyjerez

© 2022 - 2024 — McMap. All rights reserved.