C++ global single pass frustrations

Programming, for all ages and all languages.
Post Reply
User avatar
AndrewAPrice
Member
Member
Posts: 2300
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

C++ global single pass frustrations

Post 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]
My OS is Perception.
User avatar
iansjack
Member
Member
Posts: 4703
Joined: Sat Mar 31, 2012 3:07 am
Location: Chichester, UK

Re: C++ global single pass frustrations

Post by iansjack »

Wouldn't a forward declaration work in the example you give?
Korona
Member
Member
Posts: 1000
Joined: Thu May 17, 2007 1:27 pm
Contact:

Re: C++ global single pass frustrations

Post by Korona »

Huh? Why can't you just forward declare the structs in your first example?
managarm: Microkernel-based OS capable of running a Wayland desktop (Discord: https://discord.gg/7WB6Ur3). My OS-dev projects: [mlibc: Portable C library for managarm, qword, Linux, Sigma, ...] [LAI: AML interpreter] [xbstrap: Build system for OS distributions].
User avatar
Solar
Member
Member
Posts: 7615
Joined: Thu Nov 16, 2006 12:01 pm
Location: Germany
Contact:

Re: C++ global single pass frustrations

Post 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)
Every good solution is obvious once you've found it.
PeterX
Member
Member
Posts: 590
Joined: Fri Nov 22, 2019 5:46 am

Re: C++ global single pass frustrations

Post 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
kzinti
Member
Member
Posts: 898
Joined: Mon Feb 02, 2015 7:11 pm

Re: C++ global single pass frustrations

Post 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.
vvaltchev
Member
Member
Posts: 274
Joined: Fri May 11, 2018 6:51 am

Re: C++ global single pass frustrations

Post 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
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
User avatar
Velko
Member
Member
Posts: 153
Joined: Fri Oct 03, 2008 4:13 am
Location: Ogre, Latvia, EU

Re: C++ global single pass frustrations

Post 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.
If something looks overcomplicated, most likely it is.
Gigasoft
Member
Member
Posts: 856
Joined: Sat Nov 21, 2009 5:11 pm

Re: C++ global single pass frustrations

Post by Gigasoft »

Sounds like a case of needing to get your money back from university.
User avatar
Schol-R-LEA
Member
Member
Posts: 1925
Joined: Fri Oct 27, 2006 9:42 am
Location: Athens, GA, USA

Re: C++ global single pass frustrations

Post 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.
Rev. First Speaker Schol-R-LEA;2 LCF ELF JAM POEE KoR KCO PPWMTF
Ordo OS Project
Lisp programmers tend to seem very odd to outsiders, just like anyone else who has had a religious experience they can't quite explain to others.
kzinti
Member
Member
Posts: 898
Joined: Mon Feb 02, 2015 7:11 pm

Re: C++ global single pass frustrations

Post 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.
User avatar
AndrewAPrice
Member
Member
Posts: 2300
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Re: C++ global single pass frustrations

Post 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!
My OS is Perception.
vvaltchev
Member
Member
Posts: 274
Joined: Fri May 11, 2018 6:51 am

Re: C++ global single pass frustrations

Post 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.
Last edited by vvaltchev on Fri Feb 26, 2021 4:14 pm, edited 1 time in total.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
moonchild
Member
Member
Posts: 73
Joined: Wed Apr 01, 2020 4:59 pm
Libera.chat IRC: moon-child

Re: C++ global single pass frustrations

Post 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).
Post Reply