OSDev.org

The Place to Start for Operating System Developers
It is currently Thu Mar 28, 2024 3:18 am

All times are UTC - 6 hours




Post new topic Reply to topic  [ 14 posts ] 
Author Message
 Post subject: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 10:29 am 
Offline
Member
Member
User avatar

Joined: Mon Jun 05, 2006 11:00 pm
Posts: 2293
Location: USA (and Australia)
This has annoyed me for a while. In C++ it's not possible to do this:

Code:
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:
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:
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:
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.


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 10:38 am 
Offline
Member
Member
User avatar

Joined: Sat Mar 31, 2012 3:07 am
Posts: 4591
Location: Chichester, UK
Wouldn't a forward declaration work in the example you give?


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 10:39 am 
Offline
Member
Member

Joined: Thu May 17, 2007 1:27 pm
Posts: 999
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].


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 10:47 am 
Offline
Member
Member
User avatar

Joined: Thu Nov 16, 2006 12:01 pm
Posts: 7612
Location: Germany
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.


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 12:15 pm 
Offline
Member
Member

Joined: Fri Nov 22, 2019 5:46 am
Posts: 590
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


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 12:33 pm 
Offline
Member
Member

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

_________________
https://github.com/kiznit/rainbow-os


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Tue Feb 23, 2021 1:46 pm 
Offline
Member
Member

Joined: Fri May 11, 2018 6:51 am
Posts: 274
AndrewAPrice wrote:
This has annoyed me for a while. In C++ it's not possible to do this:
Code:
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:
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


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Wed Feb 24, 2021 3:19 am 
Offline
Member
Member
User avatar

Joined: Fri Oct 03, 2008 4:13 am
Posts: 153
Location: Ogre, Latvia, EU
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:
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.


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Wed Feb 24, 2021 3:20 am 
Offline
Member
Member

Joined: Sat Nov 21, 2009 5:11 pm
Posts: 852
Sounds like a case of needing to get your money back from university.


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Wed Feb 24, 2021 9:26 am 
Offline
Member
Member
User avatar

Joined: Fri Oct 27, 2006 9:42 am
Posts: 1925
Location: Athens, GA, USA
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:
(define-syntax while
  ; a simple while loop
  (syntax-rules ()
    ((while condition expr1 expr2 ...)
     (let loop ()
       (if condition
           (begin expr1 expr2 ... (loop)))))))


Code:
> (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.


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Wed Feb 24, 2021 11:41 am 
Offline
Member
Member

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

_________________
https://github.com/kiznit/rainbow-os


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Thu Feb 25, 2021 4:41 pm 
Offline
Member
Member
User avatar

Joined: Mon Jun 05, 2006 11:00 pm
Posts: 2293
Location: USA (and Australia)
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:
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.


Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Fri Feb 26, 2021 5:28 am 
Offline
Member
Member

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

_________________
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck


Last edited by vvaltchev on Fri Feb 26, 2021 4:14 pm, edited 1 time in total.

Top
 Profile  
 
 Post subject: Re: C++ global single pass frustrations
PostPosted: Fri Feb 26, 2021 4:02 pm 
Offline
Member
Member

Joined: Wed Apr 01, 2020 4:59 pm
Posts: 73
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).


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 14 posts ] 

All times are UTC - 6 hours


Who is online

Users browsing this forum: No registered users and 30 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group