Page 1 of 1

C++ global single pass frustrations

Posted: Tue Feb 23, 2021 10:29 am
by AndrewAPrice
This has annoyed me for a while. In C++ it's not possible to do this:

Code: Select all

class Dog {
 Cat TransformIntoACat();
};

class Cat {
 Dog TransformIntoADog();
};
I can't declare "class Cat;" at the top, because TransformIntoACat() returns Cat as a value.

C++ doesn't have partial classes, so I can't add "TransformIntoACat" after both Cat and Dog is defined.

I could return a pointer, but I'd have to allocate memory for Cat/Dog.

As an alternative I could built a transformer functions:

Code: Select all

Cat ConvertDogToCat(const Dog& dog);
Dog ConvertCatToDog(const Cat& cat);
Or, take a reference to Cat or Dog as a constructor arguments.

If C++ wasn't single pass (outside of classes/structs), it could see that Cat/Dog was defined later.

This is frustrating because I'm building an IDL->C++ code generator, and references to objects are passed around as {buffer, offset}, and types can reference each other in my IDL, e.g.: An Expression object can contain an Addition operator, which can contain 2 Expression operators, and in an ideal world, this would generate code such as:

Code: Select all

class Expression {
public:
  AdditionOperation GetAdditionOperation();

private:
  Buffer* buffer;
  size_t offset;
};

class AdditionOperation {
public:
  Expression GetSideA();
  Expression GetSideB();

private:
  Buffer* buffer;
  size_t offset;
};
Because it would be nice to be able to do: expression.GetAdditionOperation().GetSideA();

But, instead I have to fake it with:

Code: Select all

template <class T>
code Ref<T> {
public:
  T* operator::->() {
    return static_cast<T*>(this);
  }

private:
  Buffer* buffer;
  size_t offset;
}

class AdditionOperation;

class Expression {
public:
  Ref<AdditionOperation> GetAdditionOperation();

private:
  Buffer* buffer;
  size_t offset;
};

class AdditionOperation {
public:
  Ref<Expression> GetSideA();
  Ref<Expression> GetSideB();

private:
  Buffer* buffer;
  size_t offset;
};
Which at least gets me: expression->GetAdditionOperation()->GetSideA();

But, ducktyping Ref<T> into T seems "wrong" although it works.

I know C++ uses a multi-pass compiler because you don't need to use forward declarations inside of classes and structs, but it's frustrating that for declarations outside of a class it's acts like a single pass compiler.
[/rant]

Re: C++ global single pass frustrations

Posted: Tue Feb 23, 2021 10:38 am
by iansjack
Wouldn't a forward declaration work in the example you give?

Re: C++ global single pass frustrations

Posted: Tue Feb 23, 2021 10:39 am
by Korona
Huh? Why can't you just forward declare the structs in your first example?

Re: C++ global single pass frustrations

Posted: Tue Feb 23, 2021 10:47 am
by Solar
Hm. You address quite a few rather different issues in your post, which makes it difficult to address it in whole.

The Cat / Dog example is a bad one for the issues you bring up subsequently. It's simply not in the authority of the Dog class to transform to Cat. I'd probably aim for a Cat( Dog & ) conversion constructor.

Your IDL -> C++ example and what you actually want to do is a bit confused to me, I can't really make out what it is you want / need. But C++ is single pass, so wishing it weren't does not lead to results.

Generally speaking (and this isn't with regards to C++ specifically, but any language) as soon as you are at the point of "if only this language X were more like language Y", you're halfway down the wrong direction of a one-way road. Define clearly what it is you want to achieve in language X. Don't try to specify it in terms of language Y, because X isn't Y. Ask yourself (or someone familiar with X) how to best achieve what you want in X. Don't be halfway down the wrong road, because that adds several things to helping you -- telling you that you are going the wrong direction, why it is the wrong direction, and leading you back to the starting point.

Instead of just telling you the best way from your starting point to where you want to go. The "languages" variant of the XY problem, so to speak.

Plus, people might suspect you trying to bash a bit on the perceived shortcomings of language X, and we all know where that ends up. 8)

Re: C++ global single pass frustrations

Posted: Tue Feb 23, 2021 12:15 pm
by PeterX
Solar wrote:Generally speaking (and this isn't with regards to C++ specifically, but any language) as soon as you are at the point of "if only this language X were more like language Y", you're halfway down the wrong direction of a one-way road. Define clearly what it is you want to achieve in language X. Don't try to specify it in terms of language Y, because X isn't Y. Ask yourself (or someone familiar with X) how to best achieve what you want in X. Don't be halfway down the wrong road, because that adds several things to helping you -- telling you that you are going the wrong direction, why it is the wrong direction, and leading you back to the starting point.

Instead of just telling you the best way from your starting point to where you want to go. The "languages" variant of the XY problem, so to speak.
Very true. I once tried to implement a loop in Scheme... (It's impossible, solution: tail recursion.)

I dare to add my two cent: I have the impression that this is one of the rare cases where someone is thinking too complicated. I'm quite sure that the real problem behind the cat dog issue can be solved with simple C++ techniques. I may be wrong, but I have the strong feeling that transformation is not needed. But prove me wrong... :)

Greetings
Peter

Re: C++ global single pass frustrations

Posted: Tue Feb 23, 2021 12:33 pm
by kzinti
You probably don't want to return Cats and Dogs by value, this would be inefficient for any non-trivial class.

This would be more idiomatic in C++:

Code: Select all

class Dog
{
    Dog(const Cat& cat);
};

class Cat
{
    Cat(const Dog& cat);
};
And the implementation of the constructors go into .cpp files... No more circular dependency problems.

Re: C++ global single pass frustrations

Posted: Tue Feb 23, 2021 1:46 pm
by vvaltchev
AndrewAPrice wrote:This has annoyed me for a while. In C++ it's not possible to do this:

Code: Select all

class Dog {
 Cat TransformIntoACat();
};

class Cat {
 Dog TransformIntoADog();
};
I can't declare "class Cat;" at the top, because TransformIntoACat() returns Cat as a value.
Let's forget about design patterns etc. Actually, in C++ you can do that.
Check the example on compiler explorer: https://gcc.godbolt.org/z/KKfhTT

Code: Select all

class Cat;

class Dog {
public:
    Cat TransformIntoACat();
};

class Cat {
public:
    Dog TransformIntoADog();
};

Cat Dog::TransformIntoACat() {
    return Cat();
}

Dog Cat::TransformIntoADog() {
    return Dog();
}

int main(int argc, char **argv) {
    return 0;
}
It compiles without any problems. Just, don't implement the methods in-line.

Vlad

Re: C++ global single pass frustrations

Posted: Wed Feb 24, 2021 3:19 am
by Velko
kzinti wrote:You probably don't want to return Cats and Dogs by value, this would be inefficient for any non-trivial class.
That's what move constructors are for. Certain shortcuts for efficiency can be taken when it is known that the source will cease to exist after the call. For example, you can copy pointers to internally allocated memory to the new instance and set it to NULLs in the original.

The move semantics can also be used for conversion. For example:

Code: Select all

class Cat
{
    Cat(Dog&& dog);
};
You ensure that you will not keep the original Dog. Note that it should take a non-const reference, as the source might need modifications to be "finalized" properly.

Re: C++ global single pass frustrations

Posted: Wed Feb 24, 2021 3:20 am
by Gigasoft
Sounds like a case of needing to get your money back from university.

Re: C++ global single pass frustrations

Posted: Wed Feb 24, 2021 9:26 am
by Schol-R-LEA
PeterX wrote: Very true. I once tried to implement a loop in Scheme... (It's impossible, solution: tail recursion.)
Depends on what you mean by 'impossible'; one could always write a macro which transforms a loop idiom into a tail recursion, after all.

Indeed, there is a standard do loop syntax which works just this way... though it is insanely baroque and not at all recommended. I am not surprised if you never came across do because, first off most instructors never have either, and second off, the syntax for it is a mess. I am pretty sure that it was deliberately designed that way to discourage its use.

Also, while Scheme doesn't enforce functional programming, it does strongly encourage it, and conventional iteration is all about assignments and other side effects. Iteration syntax would just sort of go against the grain. Even 'iterations' in Scheme are done using tail recursion.

Even so, a macro for a more conventional-looking while statement isn't too difficult to write - if you've learned how to write Scheme macros, which most courses and even many textbooks simply never cover, because macrology is more than a bit hairy.

Code: Select all

(define-syntax while
  ; a simple while loop
  (syntax-rules ()
    ((while condition expr1 expr2 ...)
     (let loop ()
       (if condition
           (begin expr1 expr2 ... (loop)))))))

Code: Select all

> (define x 1)

> (while (< x 5)
    (display x)
    (newline)
    (set! x (+ 1 x)))

--> 1 2 3 4
Anyway, end of digression.

Re: C++ global single pass frustrations

Posted: Wed Feb 24, 2021 11:41 am
by kzinti
Velko wrote:That's what move constructors are for. (..)You ensure that you will not keep the original Dog. Note that it should take a non-const reference, as the source might need modifications to be "finalized" properly.
I think you might be confusing things here. The original design wasn't destroying the Dog when creating the Cat. Move constructors don't help in the OP's scenario.

Also using a move constructor between two unrelated classes seems... unusual. How can you provide move semantics between two completely unrelated classes? Unless you know something about the internals of Cat and Dog, I am not sure this makes sense. Also you won't be able to access the private members without making the classes friends... Sounds like a slippery slope.

Re: C++ global single pass frustrations

Posted: Thu Feb 25, 2021 4:41 pm
by AndrewAPrice
vvaltchev wrote:Let's forget about design patterns etc. Actually, in C++ you can do that.
Check the example on compiler explorer: https://gcc.godbolt.org/z/KKfhTT

Code: Select all

class Cat;

class Dog {
public:
    Cat TransformIntoACat();
};

class Cat {
public:
    Dog TransformIntoADog();
};

Cat Dog::TransformIntoACat() {
    return Cat();
}

Dog Cat::TransformIntoADog() {
    return Dog();
}

int main(int argc, char **argv) {
    return 0;
}
It compiles without any problems. Just, don't implement the methods in-line.
You are right! Now I'm very embarrased.

I always assumed the compiler couldn't figure out the function signature without knowing the type sizes, but I was wrong (we only need to know the type sizes at calling and implementation time, not when declaring the function!) Thanks!

Re: C++ global single pass frustrations

Posted: Fri Feb 26, 2021 5:28 am
by vvaltchev
AndrewAPrice wrote:You are right! Now I'm very embarrased.
There's nothing to be embarrassed about, man! Everybody can make a mistake ;-)
It happened to me, many times, as well. And yeah, C++ is not exactly a straightforward language. It's full of traps and pitfalls.

Re: C++ global single pass frustrations

Posted: Fri Feb 26, 2021 4:02 pm
by moonchild
PeterX wrote:Very true. I once tried to implement a loop in Scheme... (It's impossible, solution: tail recursion.)
You can do loops in scheme. Many c-style loops can be done with do. And the common lisp loop macro, which is even more powerful, has been implemented in scheme (albeit in terms of unhygienic macros which, though widely supported, are not standard).