Περάστε από την τιμή. Διαβίβαση παραμέτρων με αναφορά και κατά τιμή. Προεπιλεγμένες ρυθμίσεις

Έστω λοιπόν το Factorial(n) να είναι συνάρτηση για τον υπολογισμό του παραγοντικού ενός αριθμού n. Στη συνέχεια, δεδομένου ότι «γνωρίζουμε» ότι το παραγοντικό 1 είναι 1, μπορούμε να κατασκευάσουμε την ακόλουθη αλυσίδα:

Factorial(4)= Factorial(3)*4

Factorial(3)=Factorial(2)*3

Factorial(2)= Factorial(1)*2

Αλλά, αν δεν είχαμε μια τερματική συνθήκη ότι όταν n=1 η συνάρτηση Factorial θα επέστρεφε 1, τότε μια τέτοια θεωρητική αλυσίδα δεν θα είχε τελειώσει ποτέ, και αυτό θα μπορούσε να ήταν ένα σφάλμα υπερχείλισης στοίβας κλήσεων - υπερχείλιση στοίβας κλήσεων. Για να κατανοήσουμε τι είναι μια στοίβα κλήσεων και πώς μπορεί να ξεχειλίσει, ας δούμε την αναδρομική υλοποίηση της συνάρτησής μας:

Συνάρτηση παραγοντικό(n: Integer): LongInt;

Αν n=1 Τότε

Παραγοντικός:=Παραγωγικός(n-1)*n;

Τέλος;

Όπως βλέπουμε, για να λειτουργεί σωστά η αλυσίδα, πριν από κάθε επόμενη κλήση συνάρτησης στον εαυτό της, είναι απαραίτητο να αποθηκεύονται κάπου όλες οι τοπικές μεταβλητές, έτσι ώστε όταν αντιστραφεί η αλυσίδα, το αποτέλεσμα να είναι σωστό (η υπολογισμένη τιμή του παραγοντικού του n-1 πολλαπλασιάζεται επί n ). Στην περίπτωσή μας, κάθε φορά που η παραγοντική συνάρτηση καλείται από μόνη της, όλες οι τιμές της μεταβλητής n πρέπει να αποθηκεύονται. Η περιοχή στην οποία αποθηκεύονται οι τοπικές μεταβλητές μιας συνάρτησης όταν καλείται η ίδια αναδρομικά ονομάζεται στοίβα κλήσεων. Φυσικά, αυτή η στοίβα δεν είναι άπειρη και μπορεί να εξαντληθεί εάν οι αναδρομικές κλήσεις έχουν κατασκευαστεί λανθασμένα. Το πεπερασμένο των επαναλήψεων του παραδείγματός μας διασφαλίζεται από το γεγονός ότι όταν n=1 σταματά η κλήση της συνάρτησης.

Διαβίβαση παραμέτρων κατά τιμή και με αναφορά

Μέχρι τώρα δεν μπορούσαμε να αλλάξουμε την τιμή στην υπορουτίνα πραγματική παράμετρο(δηλαδή, η παράμετρος που καθορίζεται κατά την κλήση της υπορουτίνας) και σε ορισμένες εργασίες εφαρμογής αυτό θα ήταν βολικό. Ας θυμηθούμε τη διαδικασία Val, η οποία αλλάζει την τιμή δύο από τις πραγματικές της παραμέτρους ταυτόχρονα: η πρώτη είναι η παράμετρος όπου θα γραφεί η μετατρεπόμενη τιμή της μεταβλητής συμβολοσειράς και η δεύτερη είναι η παράμετρος Code, όπου ο αριθμός των λανθασμένων Ο χαρακτήρας τοποθετείται σε περίπτωση αστοχίας κατά τη μετατροπή τύπου. Εκείνοι. υπάρχει ακόμα ένας μηχανισμός με τον οποίο μια υπορουτίνα μπορεί να αλλάξει τις πραγματικές παραμέτρους. Αυτό είναι δυνατό χάρη σε διάφορους τρόπους μετάδοσης παραμέτρων. Ας ρίξουμε μια πιο προσεκτική ματιά σε αυτές τις μεθόδους.

Προγραμματισμός σε Pascal

Διαβίβαση παραμέτρων κατά τιμή

Ουσιαστικά, έτσι περάσαμε όλες τις παραμέτρους στις ρουτίνες μας. Ο μηχανισμός έχει ως εξής: όταν καθορίζεται μια πραγματική παράμετρος, η τιμή της αντιγράφεται στην περιοχή μνήμης όπου βρίσκεται η υπορουτίνα και στη συνέχεια, αφού η συνάρτηση ή η διαδικασία έχει ολοκληρώσει την εργασία της, αυτή η περιοχή διαγράφεται. Σε γενικές γραμμές, ενώ εκτελείται μια υπορουτίνα, υπάρχουν δύο αντίγραφα των παραμέτρων της: το ένα στο πεδίο εφαρμογής του καλούντος προγράμματος και το δεύτερο στο πεδίο εφαρμογής της συνάρτησης.

Με αυτήν τη μέθοδο μετάδοσης παραμέτρων, χρειάζεται περισσότερος χρόνος για να καλέσετε την υπορουτίνα, καθώς εκτός από την ίδια την κλήση, είναι απαραίτητο να αντιγράψετε όλες τις τιμές όλων των πραγματικών παραμέτρων. Εάν μεταβιβαστεί μεγάλος όγκος δεδομένων στην υπορουτίνα (για παράδειγμα, ένας πίνακας με μεγάλο αριθμό στοιχείων), ο χρόνος που απαιτείται για την αντιγραφή των δεδομένων στην τοπική περιοχή μπορεί να είναι σημαντικός και αυτό πρέπει να λαμβάνεται υπόψη κατά την ανάπτυξη προγραμμάτων και βρίσκοντας σημεία συμφόρησης στην απόδοσή τους.

Με αυτήν τη μέθοδο μεταφοράς, οι πραγματικές παράμετροι δεν μπορούν να αλλάξουν από την υπορουτίνα, καθώς οι αλλαγές θα επηρεάσουν μόνο μια απομονωμένη τοπική περιοχή, η οποία θα απελευθερωθεί μετά την ολοκλήρωση της λειτουργίας ή της διαδικασίας.

Διαβίβαση παραμέτρων με αναφορά

Με αυτήν τη μέθοδο, οι τιμές των πραγματικών παραμέτρων δεν αντιγράφονται στην υπορουτίνα, αλλά μεταφέρονται οι διευθύνσεις στη μνήμη (σύνδεσμοι σε μεταβλητές) όπου βρίσκονται. Σε αυτήν την περίπτωση, η υπορουτίνα αλλάζει ήδη τιμές που δεν βρίσκονται στο τοπικό πεδίο, επομένως όλες οι αλλαγές θα είναι ορατές στο πρόγραμμα κλήσης.

Για να υποδείξετε ότι ένα όρισμα πρέπει να περάσει με αναφορά, η λέξη-κλειδί var προστίθεται πριν από τη δήλωσή της:

Διαδικασία getTwoRandom(var n1, n2:Integer; range: Integer);

n1:=τυχαία(εύρος);

n2:=τυχαία(εύρος); τέλος ;

var rand1, rand2: Ακέραιος;

Αρχίζουν getTwoRandom(rand1,rand2,10); WriteLn(rand1); WriteLn(rand2);

Τέλος.

Σε αυτό το παράδειγμα, οι αναφορές σε δύο μεταβλητές μεταβιβάζονται στη διαδικασία getTwoRandom ως πραγματικές παράμετροι: rand1 και rand2. Η τρίτη πραγματική παράμετρος (10) μεταβιβάζεται με τιμή. Η διαδικασία γράφει χρησιμοποιώντας επίσημες παραμέτρους

Μέθοδοι προγραμματισμού με χρήση συμβολοσειρών

Σκοπός εργαστηριακής εργασίας : μάθετε μεθόδους στη γλώσσα C#, κανόνες για την εργασία με δεδομένα χαρακτήρων και το στοιχείο ListBox. Γράψτε ένα πρόγραμμα για εργασία με συμβολοσειρές.

Μέθοδοι

Μια μέθοδος είναι ένα στοιχείο κλάσης που περιέχει κώδικα προγράμματος. Η μέθοδος έχει την εξής δομή:

[χαρακτηριστικά] [προσδιοριστές] όνομα τύπου ([παράμετροι])

Σώμα μεθόδου;

Τα χαρακτηριστικά είναι ειδικές οδηγίες προς τον μεταγλωττιστή σχετικά με τις ιδιότητες μιας μεθόδου. Τα χαρακτηριστικά χρησιμοποιούνται σπάνια.

Τα προκριματικά είναι λέξεις-κλειδιά που εξυπηρετούν διαφορετικούς σκοπούς, για παράδειγμα:

· Προσδιορισμός της διαθεσιμότητας μιας μεθόδου για άλλες κλάσεις:

ο ιδιωτικός– η μέθοδος θα είναι διαθέσιμη μόνο σε αυτήν την κατηγορία

ο προστατεύονται– η μέθοδος θα είναι διαθέσιμη και σε παιδικές τάξεις

ο δημόσιο– η μέθοδος θα είναι διαθέσιμη σε οποιαδήποτε άλλη κλάση που έχει πρόσβαση σε αυτήν την κλάση

Ένδειξη της διαθεσιμότητας μιας μεθόδου χωρίς τη δημιουργία κλάσης

· Τύπος ρύθμισης

Ο τύπος καθορίζει το αποτέλεσμα που επιστρέφει η μέθοδος: αυτός μπορεί να είναι οποιοσδήποτε διαθέσιμος τύπος σε C#, καθώς και η λέξη-κλειδί void εάν το αποτέλεσμα δεν απαιτείται.

Το όνομα της μεθόδου είναι το αναγνωριστικό που θα χρησιμοποιηθεί για την κλήση της μεθόδου. Οι ίδιες απαιτήσεις ισχύουν για ένα αναγνωριστικό όπως και για τα ονόματα μεταβλητών: μπορεί να αποτελείται από γράμματα, αριθμούς και μια υπογράμμιση, αλλά δεν μπορεί να ξεκινά με έναν αριθμό.

Οι παράμετροι είναι μια λίστα μεταβλητών που μπορούν να περάσουν σε μια μέθοδο όταν καλούνται. Κάθε παράμετρος αποτελείται από έναν τύπο και όνομα μεταβλητής. Οι παράμετροι χωρίζονται με κόμμα.

Το σώμα μιας μεθόδου είναι ο κανονικός κώδικας προγράμματος, εκτός από το ότι δεν μπορεί να περιέχει ορισμούς άλλων μεθόδων, κλάσεων, χώρων ονομάτων κ.λπ. Εάν μια μέθοδος πρέπει να επιστρέψει κάποιο αποτέλεσμα, τότε η λέξη-κλειδί επιστροφής πρέπει να υπάρχει στο τέλος με την επιστρεφόμενη τιμή. . Εάν η επιστροφή αποτελεσμάτων δεν είναι απαραίτητη, τότε η χρήση της λέξης-κλειδιού επιστροφής δεν είναι απαραίτητη, αν και επιτρέπεται.

Ένα παράδειγμα μεθόδου που αξιολογεί μια έκφραση:

δημόσιος διπλός Υπολογισμός (διπλός α, διπλός β, διπλός γ)

επιστροφή Math.Sin(a) * Math.Cos(b);

διπλό k = Math.Tan(a * b);

επιστροφή k * Math.Exp(c / k);

Υπερφόρτωση μεθόδου

Η γλώσσα C# σας επιτρέπει να δημιουργήσετε πολλές μεθόδους με τα ίδια ονόματα αλλά διαφορετικές παραμέτρους. Ο μεταγλωττιστής θα επιλέξει αυτόματα την καταλληλότερη μέθοδο κατά την κατασκευή του προγράμματος. Για παράδειγμα, θα μπορούσατε να γράψετε δύο ξεχωριστές μεθόδους για την αύξηση ενός αριθμού σε δύναμη: ένας αλγόριθμος θα χρησιμοποιηθεί για ακέραιους αριθμούς και ένας άλλος για πραγματικούς αριθμούς:

///

/// Υπολογίστε το X στη δύναμη του Y για ακέραιους αριθμούς

///

ιδιωτικό int Pow(int X, int Y)

///

/// Υπολογίστε το X στη δύναμη του Y για πραγματικούς αριθμούς

///

ιδιωτικό διπλό Pow (διπλό X, διπλό Y)

επιστροφή Math.Exp(Y * Math.Log(Math.Abs(X)));

αλλιώς αν (Y == 0)

Αυτός ο κώδικας καλείται με τον ίδιο τρόπο, η μόνη διαφορά είναι στις παραμέτρους - στην πρώτη περίπτωση, ο μεταγλωττιστής θα καλέσει τη μέθοδο Pow με ακέραιες παραμέτρους και στη δεύτερη - με πραγματικές παραμέτρους:

Προεπιλεγμένες ρυθμίσεις

Η γλώσσα C#, ξεκινώντας από την έκδοση 4.0 (Visual Studio 2010), σας επιτρέπει να ορίσετε προεπιλεγμένες τιμές για ορισμένες παραμέτρους, έτσι ώστε κατά την κλήση μιας μεθόδου να μπορείτε να παραλείψετε ορισμένες από τις παραμέτρους. Για να γίνει αυτό, κατά την εφαρμογή της μεθόδου, στις απαιτούμενες παραμέτρους θα πρέπει να εκχωρηθεί μια τιμή απευθείας στη λίστα παραμέτρων:

ιδιωτικό κενό GetData(int Αριθμός, int Προαιρετικό = 5 )

Console.WriteLine("Number: (0)", Number);

Console.WriteLine("Προαιρετικό: (0)", Προαιρετικό);

Σε αυτήν την περίπτωση, μπορείτε να καλέσετε τη μέθοδο ως εξής:

GetData(10, 20);

Στην πρώτη περίπτωση, η παράμετρος Προαιρετική θα είναι ίση με 20, αφού ορίζεται ρητά, και στη δεύτερη περίπτωση, θα είναι ίση με 5, επειδή δεν καθορίζεται ρητά και ο μεταγλωττιστής παίρνει την προεπιλεγμένη τιμή.

Οι προεπιλεγμένες παράμετροι μπορούν να οριστούν μόνο στη δεξιά πλευρά της λίστας παραμέτρων· για παράδειγμα, μια τέτοια υπογραφή μεθόδου δεν θα γίνει αποδεκτή από τον μεταγλωττιστή:

private void GetData(int Προαιρετικό = 5 , int αριθμός)

Όταν οι παράμετροι μεταβιβάζονται σε μια μέθοδο με τον κανονικό τρόπο (χωρίς τις πρόσθετες λέξεις-κλειδιά αναφοράς και εξόδου), τυχόν αλλαγές στις παραμέτρους της μεθόδου δεν επηρεάζουν την τιμή της στο κύριο πρόγραμμα. Ας πούμε ότι έχουμε την εξής μέθοδο:

ιδιωτικό κενό Υπολογισμός (int Αριθμός)

Μπορεί να φανεί ότι μέσα στη μέθοδο η μεταβλητή Number, η οποία μεταβιβάστηκε ως παράμετρος, αλλάζει. Ας προσπαθήσουμε να καλέσουμε τη μέθοδο:

Console.WriteLine(n);

Στην οθόνη θα εμφανιστεί ο αριθμός 1, δηλαδή, παρά την αλλαγή της μεταβλητής στη μέθοδο Calc, η τιμή της μεταβλητής στο κύριο πρόγραμμα δεν έχει αλλάξει. Αυτό οφείλεται στο γεγονός ότι όταν καλείται μια μέθοδος, α αντίγραφοπέρασε τη μεταβλητή, είναι αυτή η μεταβλητή που αλλάζει η μέθοδος. Όταν η μέθοδος τερματιστεί, η αξία των αντιγράφων χάνεται. Αυτή η μέθοδος μετάδοσης μιας παραμέτρου ονομάζεται περάσουν από την αξία.

Για να αλλάξει μια μέθοδος μια μεταβλητή που της μεταβιβάστηκε, πρέπει να περάσει με τη λέξη-κλειδί ref - πρέπει να βρίσκεται και στην υπογραφή της μεθόδου και όταν καλείται:

ιδιωτικό κενό Υπολογισμός (αναφ. int αριθμός)

Console.WriteLine(n);

Σε αυτήν την περίπτωση, ο αριθμός 10 θα εμφανιστεί στην οθόνη: η αλλαγή της τιμής στη μέθοδο επηρέασε επίσης το κύριο πρόγραμμα. Αυτή η μέθοδος μεταφορά ονομάζεται περνώντας από αναφορά, δηλ. Δεν είναι πλέον ένα αντίγραφο που μεταδίδεται, αλλά μια αναφορά σε μια πραγματική μεταβλητή στη μνήμη.

Εάν μια μέθοδος χρησιμοποιεί μεταβλητές με αναφορά μόνο για να επιστρέψει τιμές και δεν της ενδιαφέρει τι περιείχε αρχικά, τότε δεν μπορείτε να αρχικοποιήσετε τέτοιες μεταβλητές, αλλά να τις μεταβιβάσετε με τη λέξη-κλειδί out. Ο μεταγλωττιστής κατανοεί ότι η αρχική τιμή της μεταβλητής δεν είναι σημαντική και δεν παραπονιέται για την έλλειψη αρχικοποίησης:

ιδιωτικό κενό Υπολογισμός (out int αριθμός)

int n; // Δεν αναθέτουμε τίποτα!

τύπος δεδομένων συμβολοσειράς

Η γλώσσα C# χρησιμοποιεί τον τύπο συμβολοσειράς για την αποθήκευση συμβολοσειρών. Για να δηλώσετε (και, κατά κανόνα, να αρχικοποιήσετε αμέσως) μια μεταβλητή συμβολοσειράς, μπορείτε να γράψετε τον ακόλουθο κώδικα:

string a = "Κείμενο";

string b = "strings";

Μπορείτε να εκτελέσετε μια λειτουργία προσθήκης σε γραμμές - σε αυτήν την περίπτωση, το κείμενο μιας γραμμής θα προστεθεί στο κείμενο μιας άλλης:

συμβολοσειρά c = a + " " + b; // Αποτέλεσμα: Κείμενο συμβολοσειράς

Ο τύπος συμβολοσειράς είναι στην πραγματικότητα ένα ψευδώνυμο για την κλάση String, η οποία σας επιτρέπει να εκτελέσετε έναν αριθμό πιο σύνθετων λειτουργιών σε συμβολοσειρές. Για παράδειγμα, η μέθοδος IndexOf μπορεί να αναζητήσει μια υποσυμβολοσειρά σε μια συμβολοσειρά και η μέθοδος Substring επιστρέφει ένα τμήμα της συμβολοσειράς ενός καθορισμένου μήκους, ξεκινώντας από μια καθορισμένη θέση:

string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

int index = a.IndexOf("OP"); // Αποτέλεσμα: 14 (μετρώντας από το 0)

συμβολοσειρά b = a.Substring(3, 5); // Αποτέλεσμα: DEFGH

Εάν χρειάζεται να προσθέσετε ειδικούς χαρακτήρες σε μια συμβολοσειρά, μπορείτε να το κάνετε χρησιμοποιώντας ακολουθίες διαφυγής που ξεκινούν με ανάστροφη κάθετο:

Στοιχείο ListBox

Συστατικό ListBoxείναι μια λίστα τα στοιχεία της οποίας επιλέγονται χρησιμοποιώντας το πληκτρολόγιο ή το ποντίκι. Η λίστα των στοιχείων καθορίζεται από την ιδιότητα Είδη. Τα στοιχεία είναι ένα στοιχείο που έχει τις δικές του ιδιότητες και τις δικές του μεθόδους. Μέθοδοι Προσθήκη, RemoveAtΚαι Εισάγετεχρησιμοποιούνται για την προσθήκη, διαγραφή και εισαγωγή στοιχείων.

Ενα αντικείμενο Είδηαποθηκεύει τα αντικείμενα στη λίστα. Το αντικείμενο μπορεί να είναι οποιαδήποτε κλάση - τα δεδομένα κλάσης μετατρέπονται για εμφάνιση σε αναπαράσταση συμβολοσειράς με τη μέθοδο ToString. Στην περίπτωσή μας, οι συμβολοσειρές θα λειτουργήσουν ως αντικείμενα. Ωστόσο, δεδομένου ότι το αντικείμενο Items αποθηκεύει αντικείμενα που μεταδίδονται σε αντικείμενο τύπου, πριν το χρησιμοποιήσετε πρέπει να τα επαναφέρετε στον αρχικό τους τύπο, στην περίπτωσή μας συμβολοσειρά:

string a = (string)listBox1.Items;

Για να προσδιορίσετε τον αριθμό του επιλεγμένου στοιχείου, χρησιμοποιήστε την ιδιότητα ΕπιλεγμένοΕυρετήριο.

Όταν άρχισα να προγραμματίζω σε C++ και μελετούσα εντατικά βιβλία και άρθρα, έβρισκα πάντα την ίδια συμβουλή: αν χρειάζεται να περάσουμε κάποιο αντικείμενο σε μια συνάρτηση που δεν πρέπει να αλλάζει στη συνάρτηση, τότε πρέπει πάντα να περνάει με αναφορά σε μια σταθερά(PPSK), εκτός από εκείνες τις περιπτώσεις που πρέπει να περάσουμε είτε έναν πρωτόγονο τύπο είτε μια δομή παρόμοια σε μέγεθος με αυτά. Επειδή Για περισσότερα από 10 χρόνια προγραμματισμού σε C++, έχω συναντήσει αυτή τη συμβουλή πολύ συχνά (και ο ίδιος την έχω δώσει περισσότερες από μία φορές), έχει «απορροφηθεί» από καιρό μέσα μου - μεταφέρω αυτόματα όλα τα επιχειρήματα με αναφορά σε μια σταθερά . Όμως ο καιρός περνά και έχουν περάσει ήδη 7 χρόνια από τότε που είχαμε στη διάθεσή μας την C++11 με τη σημασιολογία της κίνησης, σε σχέση με την οποία ακούω όλο και περισσότερες φωνές να αμφισβητούν το παλιό καλό δόγμα. Πολλοί αρχίζουν να υποστηρίζουν ότι το πέρασμα με αναφορά σε μια σταθερά ανήκει στο παρελθόν και τώρα είναι απαραίτητο περάσουν από την αξία(PPZ). Τι κρύβεται πίσω από αυτές τις συζητήσεις, καθώς και ποια συμπεράσματα μπορούμε να βγάλουμε από όλα αυτά, θέλω να συζητήσω σε αυτό το άρθρο.

Βιβλιοσοφία

Για να καταλάβουμε ποιον κανόνα πρέπει να τηρούμε, προτείνω να στραφούμε στα βιβλία. Τα βιβλία είναι μια εξαιρετική πηγή πληροφοριών που δεν είμαστε υποχρεωμένοι να αποδεχτούμε, αλλά που σίγουρα αξίζει να ακούσουμε. Και θα ξεκινήσουμε με την ιστορία, με την καταγωγή. Δεν θα μάθω ποιος ήταν ο πρώτος απολογητής της PPSC, απλά θα δώσω ως παράδειγμα το βιβλίο που προσωπικά με άσκησε τη μεγαλύτερη επιρροή στο θέμα της χρήσης του PPSC.

Mayers

Εντάξει, εδώ έχουμε μια κλάση στην οποία όλες οι παράμετροι περνούν με αναφορά, υπάρχουν προβλήματα με αυτήν την κλάση; Δυστυχώς, υπάρχει, και αυτό το πρόβλημα βρίσκεται στην επιφάνεια. Έχουμε 2 λειτουργικές οντότητες στην τάξη μας: η πρώτη παίρνει μια τιμή στο στάδιο δημιουργίας αντικειμένου και η δεύτερη σας επιτρέπει να αλλάξετε μια τιμή που είχε οριστεί προηγουμένως. Έχουμε δύο οντότητες, αλλά τέσσερις λειτουργίες. Τώρα φανταστείτε ότι μπορούμε να έχουμε όχι 2 παρόμοιες οντότητες, αλλά 3, 5, 6, τι τότε; Τότε θα αντιμετωπίσουμε σοβαρή διόγκωση κώδικα. Επομένως, για να μην δημιουργηθεί μια μάζα συναρτήσεων, υπήρξε μια πρόταση να εγκαταλειφθούν εντελώς οι σύνδεσμοι στις παραμέτρους:

Πρότυπο Κατόχος κλάσης ( δημόσια: ρητή Κάτοχος (τιμή T): m_Value(move(value)) ( ) void setValue(T value) ( m_Value = move(value); ) const T& value() const noexcept ( return m_Value; ) ιδιωτικό: T m_Value; );

Το πρώτο πλεονέκτημα που τραβάει αμέσως την προσοχή σας είναι ότι υπάρχει σημαντικά λιγότερος κώδικας. Υπάρχει ακόμη λιγότερο από ό,τι στην πρώτη έκδοση, λόγω της αφαίρεσης των const και & (αν και πρόσθεσαν το move ). Αλλά πάντα μας διδάσκονταν ότι το να περνάς από αναφορά είναι πιο παραγωγικό από το να περνάς από αξία! Έτσι ήταν πριν από τη C++11, και πώς είναι ακόμα, αλλά τώρα αν κοιτάξουμε αυτόν τον κώδικα, θα δούμε ότι δεν υπάρχει περισσότερη αντιγραφή εδώ από ό,τι στην πρώτη έκδοση, υπό την προϋπόθεση ότι το Τ έχει κατασκευαστή κίνησης. Εκείνοι. Το ίδιο το PPSC ήταν και θα είναι ταχύτερο από το PPZ, αλλά ο κώδικας χρησιμοποιεί κατά κάποιον τρόπο την περασμένη αναφορά και συχνά αυτό το όρισμα αντιγράφεται.

Ωστόσο, αυτή δεν είναι όλη η ιστορία. Σε αντίθεση με την πρώτη επιλογή, όπου έχουμε μόνο αντιγραφή, εδώ προσθέτουμε και κίνηση. Αλλά η μετακόμιση είναι μια φθηνή επέμβαση, σωστά; Σε αυτό το θέμα, το βιβλίο Mayers που εξετάζουμε έχει επίσης ένα κεφάλαιο («Στοιχείο 29»), το οποίο έχει τον τίτλο: «Ας υποθέσουμε ότι οι λειτουργίες μετακίνησης δεν υπάρχουν, δεν είναι φθηνές και δεν χρησιμοποιούνται». Η κύρια ιδέα πρέπει να είναι ξεκάθαρη από τον τίτλο, αλλά αν θέλετε λεπτομέρειες, φροντίστε να τη διαβάσετε - δεν θα σταθώ σε αυτήν.

Θα ήταν σκόπιμο εδώ να γίνει μια πλήρης συγκριτική ανάλυση της πρώτης και της τελευταίας μεθόδου, αλλά δεν θα ήθελα να παρεκκλίνω από το βιβλίο, επομένως θα αναβάλουμε την ανάλυση για άλλες ενότητες και εδώ θα συνεχίσουμε να εξετάζουμε τα επιχειρήματα του Scott. Έτσι, εκτός από το γεγονός ότι η τρίτη επιλογή είναι προφανώς πιο σύντομη από τη δεύτερη, ποιο θεωρεί ο Scott ως το πλεονέκτημα του PPZ έναντι του PPSC στον σύγχρονο κώδικα;

Το βλέπει στο γεγονός ότι στην περίπτωση που περάσει μια rvalue, δηλ. Κάποιοι καλούν έτσι: Holder holder(string("me")); , η επιλογή με PPSC θα μας δώσει αντιγραφή και η επιλογή με PPZ θα μας δώσει κίνηση. Από την άλλη πλευρά, εάν η μεταφορά είναι ως εξής: Κάτοχος κατόχου (κάποια τιμή) , τότε το PPZ σίγουρα χάνει λόγω του ότι θα κάνει και αντιγραφή και μετακίνηση, ενώ στην έκδοση με PPSC θα υπάρχει μόνο μία αντιγραφή. Εκείνοι. αποδεικνύεται ότι το PPZ, αν λάβουμε υπόψη καθαρά την αποτελεσματικότητα, είναι κάποιου είδους συμβιβασμός μεταξύ της ποσότητας του κώδικα και της «πλήρους» (μέσω &&) υποστήριξης για τη σημασιολογία κίνησης.

Γι' αυτό ο Σκοτ ​​διατύπωσε τόσο προσεκτικά τις συμβουλές του και τις προωθεί τόσο προσεκτικά. Μου φάνηκε μάλιστα ότι το έθεσε απρόθυμα, σαν να ήταν υπό πίεση: δεν μπορούσε παρά να τοποθετήσει συζητήσεις για αυτό το θέμα στο βιβλίο, γιατί... συζητήθηκε αρκετά ευρέως και ο Scott ήταν πάντα συλλέκτης συλλογικής εμπειρίας. Επιπλέον, δίνει πολύ λίγα επιχειρήματα για την υπεράσπιση του PPZ, αλλά δίνει πολλά από αυτά που αμφισβητούν αυτή την «τεχνική». Θα εξετάσουμε τα επιχειρήματά του κατά στις επόμενες ενότητες, αλλά εδώ θα επαναλάβουμε εν συντομία το επιχείρημα που προβάλλει ο Scott για την υπεράσπιση της PPP (προσθέτοντας διανοητικά «αν το αντικείμενο υποστηρίζει κίνηση και είναι φθηνό»): σας επιτρέπει να αποφύγετε την αντιγραφή όταν μεταβιβάζετε μια έκφραση rvalue ως όρισμα συνάρτησης. Αλλά αρκετά βασανιστικό το βιβλίο του Meyers, ας περάσουμε σε ένα άλλο βιβλίο.

Παρεμπιπτόντως, αν κάποιος έχει διαβάσει το βιβλίο και εκπλήσσεται που δεν συμπεριλαμβάνω εδώ μια επιλογή με αυτό που ο Mayers ονόμασε καθολικές αναφορές - τώρα γνωστές ως αναφορές προώθησης - τότε αυτό εξηγείται εύκολα. Σκέφτομαι μόνο τα PPZ και PPSC, γιατί... Θεωρώ κακή μορφή να εισαγάγω συναρτήσεις προτύπου για μεθόδους που δεν είναι πρότυπα απλώς για λόγους υποστήριξης της μετάβασης με αναφορά και των δύο τύπων (rvalue/lvalue). Για να μην αναφέρουμε το γεγονός ότι ο κώδικας αποδεικνύεται διαφορετικός (όχι άλλη σταθερότητα) και φέρνει μαζί του άλλα προβλήματα.

Josattis και παρέα

Το τελευταίο βιβλίο που θα δούμε είναι το "C++ Templates", το οποίο είναι επίσης το πιο πρόσφατο από όλα τα βιβλία που αναφέρονται σε αυτό το άρθρο. Εκδόθηκε στα τέλη του 2017 (και το 2018 αναφέρεται μέσα στο βιβλίο). Σε αντίθεση με άλλα βιβλία, αυτό είναι εξ ολοκλήρου αφιερωμένο σε μοτίβα και όχι σε συμβουλές (όπως το Mayers) ή στη C++ γενικά, όπως το Stroustrup. Επομένως, τα πλεονεκτήματα/μειονεκτήματα εξετάζονται εδώ από την άποψη των προτύπων γραφής.

Ένα ολόκληρο κεφάλαιο 7 είναι αφιερωμένο σε αυτό το θέμα, το οποίο έχει τον εύγλωττο τίτλο «Με αξία ή με αναφορά;». Σε αυτό το κεφάλαιο, οι συγγραφείς περιγράφουν πολύ σύντομα αλλά συνοπτικά όλες τις μεθόδους μετάδοσης με όλα τα πλεονεκτήματα και τα μειονεκτήματά τους. Μια ανάλυση της αποτελεσματικότητας πρακτικά δεν δίνεται εδώ, και θεωρείται δεδομένο ότι το PPSC θα είναι ταχύτερο από το PPZ. Αλλά με όλα αυτά, στο τέλος του κεφαλαίου οι συγγραφείς προτείνουν τη χρήση του προεπιλεγμένου PPP για τις λειτουργίες προτύπων. Γιατί; Επειδή χρησιμοποιώντας έναν σύνδεσμο, οι παράμετροι του προτύπου εμφανίζονται πλήρως και χωρίς σύνδεσμο "αποκαθίστανται", κάτι που έχει ευεργετική επίδραση στην επεξεργασία των πινάκων και των κυριολεκτικών συμβολοσειρών. Οι συγγραφείς πιστεύουν ότι εάν για κάποιο τύπο PPP αποδειχθεί αναποτελεσματικό, τότε μπορείτε πάντα να χρησιμοποιήσετε τα std::ref και std::cref . Αυτή είναι μια συμβουλή, για να είμαι ειλικρινής, έχετε δει πολλούς ανθρώπους που θέλουν να χρησιμοποιήσουν τις παραπάνω λειτουργίες;

Τι συμβουλεύουν σχετικά με την PPSC; Συμβουλεύουν τη χρήση PPSC όταν η απόδοση είναι κρίσιμη ή υπάρχουν άλλες βαρυσήμαντοςλόγοι για να μην χρησιμοποιήσετε PPP. Φυσικά, εδώ μιλάμε μόνο για κώδικα λέβητα, αλλά αυτή η συμβουλή έρχεται σε άμεση αντίθεση με όλα όσα διδάσκονται οι προγραμματιστές εδώ και μια δεκαετία. Αυτή δεν είναι απλώς μια συμβουλή για να εξετάσετε τη ΣΔΙΤ ως εναλλακτική - όχι, αυτή είναι μια συμβουλή για να γίνει η PPSC εναλλακτική.

Αυτό ολοκληρώνει την περιοδεία μας στο βιβλίο, γιατί... Δεν γνωρίζω άλλα βιβλία που πρέπει να συμβουλευτούμε για αυτό το θέμα. Ας περάσουμε σε άλλο χώρο μέσων.

Δικτυακή σοφία

Επειδή Ζούμε στην εποχή του Διαδικτύου, τότε δεν πρέπει να βασίζεστε μόνο στη σοφία των βιβλίων. Επιπλέον, πολλοί συγγραφείς που έγραφαν βιβλία, τώρα απλώς γράφουν blog και έχουν εγκαταλείψει βιβλία. Ένας από αυτούς τους συγγραφείς είναι ο Herb Sutter, ο οποίος τον Μάιο του 2013 δημοσίευσε ένα άρθρο στο blog του «GotW #4 Solution: Class Mechanics», το οποίο, αν και δεν είναι εξ ολοκλήρου αφιερωμένο στο πρόβλημα που καλύπτουμε, εξακολουθεί να το αγγίζει.

Έτσι, στην αρχική έκδοση του άρθρου, ο Sutter απλώς επανέλαβε την παλιά σοφία: "πέρασε τις παραμέτρους με αναφορά σε μια σταθερά", αλλά δεν θα βλέπουμε πλέον αυτήν την έκδοση του άρθρου, επειδή Το άρθρο περιέχει την αντίθετη συμβουλή: « Ανη παράμετρος θα συνεχίσει να αντιγράφεται και, στη συνέχεια, θα μεταβιβαστεί με τιμή." Και πάλι το περιβόητο «αν». Γιατί ο Sutter άλλαξε το άρθρο και πώς το ήξερα; Από τα σχόλια. Διαβάστε τα σχόλια στο άρθρο του· παρεμπιπτόντως, είναι πιο ενδιαφέροντα και χρήσιμα από το ίδιο το άρθρο. Είναι αλήθεια ότι αφού έγραψε το άρθρο, ο Sutter άλλαξε τελικά τη γνώμη του και δεν δίνει πλέον τέτοιες συμβουλές. Η αλλαγή στη γνώμη μπορεί να βρεθεί στην ομιλία του στο CppCon το 2014: «Επιστροφή στα Βασικά! Βασικά στοιχεία του σύγχρονου στυλ C++». Φροντίστε να κοιτάξετε, θα προχωρήσουμε στον επόμενο σύνδεσμο Διαδικτύου.

Και στη συνέχεια έχουμε τον κύριο πόρο προγραμματισμού του 21ου αιώνα: το StackOverflow. Ή μάλλον η απάντηση, με τον αριθμό των θετικών αντιδράσεων να ξεπερνά τις 1700 τη στιγμή της συγγραφής αυτού του άρθρου. Το ερώτημα είναι: Τι είναι το ιδίωμα αντιγραφής και ανταλλαγής; , και, όπως υποδηλώνει ο τίτλος, όχι ακριβώς στο θέμα που εξετάζουμε. Στην απάντησή του όμως στο ερώτημα αυτό ο συγγραφέας θίγει και ένα θέμα που μας ενδιαφέρει. Συμβουλεύει επίσης τη χρήση του PPZ "εάν το επιχείρημα θα αντιγραφεί ούτως ή άλλως" (ήρθε η ώρα να εισαγάγουμε μια συντομογραφία και για αυτό, από τον Θεό). Και γενικά, αυτή η συμβουλή φαίνεται αρκετά κατάλληλη, στο πλαίσιο της απάντησής του και του χειριστή που συζητήθηκε εκεί, αλλά ο συγγραφέας έχει το ελεύθερο να δώσει τέτοιες συμβουλές με ευρύτερο τρόπο, και όχι μόνο στη συγκεκριμένη περίπτωση. Επιπλέον, προχωρά παραπέρα από όλες τις συμβουλές που έχουμε συζητήσει προηγουμένως και ζητά να γίνει αυτό ακόμη και σε κώδικα C++03! Τι ώθησε τον συγγραφέα να βγάλει τέτοια συμπεράσματα;

Προφανώς, ο συγγραφέας της απάντησης άντλησε την κύρια έμπνευση από ένα άρθρο άλλου συγγραφέα βιβλίων και προγραμματιστή μερικής απασχόλησης του Boost.MPL - Dave Abrahams. Το άρθρο ονομάζεται «Want Speed? Περάστε από την αξία." , και δημοσιεύτηκε τον Αύγουστο του 2009, δηλ. 2 χρόνια πριν την υιοθέτηση της C++11 και την εισαγωγή της σημασιολογίας κίνησης. Όπως σε προηγούμενες περιπτώσεις, συνιστώ στον αναγνώστη να διαβάσει το άρθρο μόνος του, αλλά θα δώσω τα κύρια επιχειρήματα (υπάρχει, στην πραγματικότητα, μόνο ένα επιχείρημα) που προβάλλει ο Dave υπέρ του PPZ: πρέπει να χρησιμοποιήσετε το PPZ , επειδή η βελτιστοποίηση "παράλειψη αντιγραφής" λειτουργεί καλά με αυτό (εξάλειψη αντιγραφής), η οποία απουσιάζει στο PPSC. Αν διαβάσετε τα σχόλια του άρθρου, μπορείτε να δείτε ότι οι συμβουλές που προωθεί δεν είναι καθολικές, κάτι που επιβεβαιώνει ο ίδιος ο συγγραφέας όταν απαντά στην κριτική των σχολιαστών. Ωστόσο, το άρθρο περιέχει ρητές συμβουλές (οδηγία) για τη χρήση του PPP εάν το επιχείρημα θα αντιγραφεί ούτως ή άλλως. Παρεμπιπτόντως, αν κάποιος ενδιαφέρεται, μπορεί να διαβάσει το άρθρο «Θέλετε ταχύτητα; Μην προσπερνάτε (πάντα) την αξία». . Όπως θα έπρεπε να υποδηλώνει ο τίτλος, αυτό το άρθρο είναι μια απάντηση στο άρθρο του Dave, οπότε αν διαβάσατε το πρώτο, φροντίστε να διαβάσετε και αυτό!

Δυστυχώς (ευτυχώς για ορισμένους), τέτοια άρθρα και (ακόμα περισσότερο) δημοφιλείς απαντήσεις σε δημοφιλείς ιστότοπους προκαλούν τη μαζική χρήση αμφίβολων τεχνικών (ένα ασήμαντο παράδειγμα) απλώς και μόνο επειδή αυτό απαιτεί λιγότερο γράψιμο και το παλιό δόγμα δεν είναι πλέον ακλόνητο - Μπορείτε πάντα να ανατρέχετε σε «αυτή τη δημοφιλή συμβουλή» εάν σας σπρώχνουν στον τοίχο. Τώρα σας προτείνω να εξοικειωθείτε με το τι μας προσφέρουν διάφοροι πόροι με συστάσεις για τη σύνταξη κώδικα.

Επειδή Δεδομένου ότι διάφορα πρότυπα και συστάσεις δημοσιεύονται πλέον και στο διαδίκτυο, αποφάσισα να ταξινομήσω αυτήν την ενότητα ως "δικτυακή σοφία". Εδώ, λοιπόν, θα ήθελα να μιλήσω για δύο πηγές, σκοπός των οποίων είναι να βελτιώσουν τον κώδικα των προγραμματιστών της C++ παρέχοντάς τους συμβουλές (οδηγίες) για το πώς να γράφουν αυτόν ακριβώς τον κώδικα.

Το πρώτο σύνολο κανόνων που θέλω να εξετάσω ήταν η σταγόνα που ξεχείλισε το ποτήρι που με ανάγκασε να ακολουθήσω αυτό το άρθρο. Αυτό το σετ είναι μέρος του βοηθητικού προγράμματος clang-tidy και δεν υπάρχει έξω από αυτό. Όπως όλα όσα σχετίζονται με το clang, αυτό το βοηθητικό πρόγραμμα είναι πολύ δημοφιλές και έχει ήδη ενσωματωθεί με το CLion και το Resharper C++ (έτσι το συνάντησα). Έτσι, το clang-tydy περιέχει έναν κανόνα εκσυγχρονισμού-περάσματος-τιμής που λειτουργεί σε κατασκευαστές που δέχονται ορίσματα μέσω PPSC. Αυτός ο κανόνας προτείνει να αντικαταστήσουμε το PPSC με το PPZ. Επιπλέον, τη στιγμή της σύνταξης του άρθρου, η περιγραφή αυτού του κανόνα περιέχει μια παρατήρηση ότι αυτός ο κανόνας Αντίολειτουργεί μόνο για κατασκευαστές, αλλά αυτοί (ποιοι είναι αυτοί;) θα δεχτούν ευχαρίστως τη βοήθεια από εκείνους που επεκτείνουν αυτόν τον κανόνα σε άλλες οντότητες. Εκεί, στην περιγραφή, υπάρχει ένας σύνδεσμος προς το άρθρο του Dave - είναι σαφές από πού προέρχονται τα πόδια.

Τέλος, για να ολοκληρώσετε αυτήν την ανασκόπηση της σοφίας και των έγκυρων απόψεων άλλων, σας προτείνω να δείτε τις επίσημες οδηγίες για τη σύνταξη κώδικα C++: C++ Core Guidelines, οι κύριοι συντάκτες των οποίων είναι οι Herb Sutter και Bjarne Stroustrup (όχι άσχημα, σωστά;). Έτσι, αυτές οι συστάσεις περιέχουν τον ακόλουθο κανόνα: "Για παραμέτρους "in", περάστε τύπους φθηνά αντιγραμμένους κατά τιμή και άλλους με αναφορά στο const", που επαναλαμβάνει πλήρως την παλιά σοφία: PPSK παντού και PPP για μικρά αντικείμενα. Αυτή η συμβουλή περιγράφει διάφορες εναλλακτικές που πρέπει να εξετάσετε. σε περίπτωση που το όρισμα χρειάζεται βελτιστοποίηση. Όμως το PPZ δεν περιλαμβάνεται στη λίστα των εναλλακτικών!

Δεδομένου ότι δεν έχω άλλες πηγές άξιες προσοχής, προτείνω να προχωρήσουμε σε μια άμεση ανάλυση και των δύο μεθόδων μετάδοσης.

Ανάλυση

Ολόκληρο το προηγούμενο κείμενο είναι γραμμένο με έναν τρόπο κάπως ασυνήθιστο για μένα: παρουσιάζω τις απόψεις άλλων και προσπαθώ ακόμη και να μην εκφράσω τις δικές μου (ξέρω ότι μου βγαίνει άσχημα). Σε μεγάλο βαθμό λόγω του γεγονότος ότι οι απόψεις άλλων, και στόχος μου ήταν να κάνω μια σύντομη επισκόπηση τους, ανέβαλα μια λεπτομερή εξέταση ορισμένων επιχειρημάτων που βρήκα σε άλλους συγγραφείς. Σε αυτή την ενότητα, δεν θα αναφερθώ σε αρχές και δεν θα εκφέρω απόψεις· εδώ θα εξετάσουμε ορισμένα αντικειμενικά πλεονεκτήματα και μειονεκτήματα των PPSC και PPZ, τα οποία θα είναι καρυκευμένα με την υποκειμενική μου αντίληψη. Φυσικά, μερικά από αυτά που συζητήθηκαν νωρίτερα θα επαναληφθούν, αλλά, δυστυχώς, αυτή είναι η δομή αυτού του άρθρου.

Έχει πλεονέκτημα η ΣΔΙΤ;

Έτσι, πριν εξετάσουμε τα επιχειρήματα υπέρ και κατά, προτείνω να δούμε τι και σε ποιες περιπτώσεις μας δίνει το πλεονέκτημα που μας δίνει το πέρασμα από την αξία. Ας υποθέσουμε ότι έχουμε μια τάξη όπως αυτή:

Κλάση CopyMover ( δημόσια: void setByValuer(Accounter byValuer) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuer_OverValuer(m_ByValuer) aluerAndNotMover = byVal uerAndNotMover; ) άκυρο setRvaluer (Accounter&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Αν και για τους σκοπούς αυτού του άρθρου μας ενδιαφέρουν μόνο οι δύο πρώτες λειτουργίες, έχω συμπεριλάβει τέσσερις επιλογές μόνο για να τις χρησιμοποιήσω ως αντίθεση.

Η κλάση Accounter είναι μια απλή κλάση που μετράει πόσες φορές έχει αντιγραφεί/μετακινηθεί. Και στην κλάση CopyMover έχουμε υλοποιήσει συναρτήσεις που μας επιτρέπουν να εξετάσουμε τις ακόλουθες επιλογές:

    κίνησηπέρασε επιχείρημα.

    Περάστε από την τιμή, ακολουθούμενη από αντιγραφήπέρασε επιχείρημα.

Τώρα, αν περάσουμε μια τιμή l σε καθεμία από αυτές τις συναρτήσεις, για παράδειγμα ως εξής:

Λογαριασμός byRefer; Λογαριασμός byValuer; Λογαριασμός από ValuerAndNotMover; CopyMover copyMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValuer); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

τότε έχουμε τα εξής αποτελέσματα:

Ο προφανής νικητής είναι η PPSC, γιατί... δίνει μόνο ένα αντίγραφο, ενώ το PPZ δίνει ένα αντίγραφο και μια κίνηση.

Τώρα ας προσπαθήσουμε να περάσουμε μια τιμή rvalue:

CopyMover copyMover; copyMover.setByRefer(Accounter()); copyMover.setByValuer(Accounter()); copyMover.setByValuerAndNotMover(Accounter()); copyMover.setRvaluer(Accounter());

Παίρνουμε τα εξής:

Δεν υπάρχει ξεκάθαρος νικητής εδώ, γιατί... Και το PPZ και το PPSK έχουν μία λειτουργία το καθένα, αλλά λόγω του γεγονότος ότι το PPZ χρησιμοποιεί κίνηση και το PPSK χρησιμοποιεί αντιγραφή, μπορούμε να δώσουμε τη νίκη στην PPZ.

Αλλά τα πειράματά μας δεν τελειώνουν εκεί· ας προσθέσουμε τις ακόλουθες συναρτήσεις για την προσομοίωση μιας έμμεσης κλήσης (με μεταγενέστερο πέρασμα ορισμάτων):

Void setByValuer(Accounter byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer)); ) void setByRefer(const Accounter& byRefer, CopyMover& copyMover) ( copyMover.setByRefer);

Θα τα χρησιμοποιήσουμε ακριβώς με τον ίδιο τρόπο που κάναμε χωρίς αυτά, οπότε δεν θα επαναλάβω τον κώδικα (αν χρειάζεται, κοιτάξτε στο αποθετήριο). Άρα για την τιμή lvalue τα αποτελέσματα θα ήταν ως εξής:

Σημειώστε ότι το PPSC αυξάνει το χάσμα με το PPZ, παραμένοντας με ένα μόνο αντίγραφο, ενώ το PPZ έχει ήδη έως και 3 λειτουργίες (μία ακόμη κίνηση)!

Τώρα περνάμε την τιμή rvalue και παίρνουμε τα ακόλουθα αποτελέσματα:

Τώρα το PPZ έχει 2 κινήσεις και το PPSC έχει ακόμα ένα αντίγραφο. Είναι πλέον δυνατό να αναδειχθεί η PPZ ως νικητής; Οχι επειδή αν μια κίνηση δεν πρέπει να είναι τουλάχιστον χειρότερη από ένα αντίγραφο, δεν μπορούμε να πούμε το ίδιο για 2 κινήσεις. Επομένως, δεν θα υπάρχει νικητής σε αυτό το παράδειγμα.

Μπορεί να μου φέρουν αντιρρήσεις: «Συγγραφέ, έχεις προκατειλημμένη γνώμη και τραβάς ό,τι σε ωφελεί. Ακόμα και 2 κινήσεις θα είναι φθηνότερες από την αντιγραφή!». Δεν μπορώ να συμφωνήσω με αυτή τη δήλωση Ολα για όλα, επειδή Το πόσο πιο γρήγορη είναι η μετακίνηση από την αντιγραφή εξαρτάται από τη συγκεκριμένη τάξη, αλλά θα εξετάσουμε τη «φθηνή» μετακίνηση σε ξεχωριστή ενότητα.

Εδώ θίξαμε ένα ενδιαφέρον πράγμα: προσθέσαμε μία έμμεση κλήση και η ΣΔΙΤ πρόσθεσε ακριβώς μία πράξη στο «βάρος». Νομίζω ότι δεν χρειάζεται να έχετε δίπλωμα από το MSTU για να καταλάβετε ότι όσο περισσότερες έμμεσες κλήσεις έχουμε, τόσο περισσότερες λειτουργίες θα εκτελούνται όταν χρησιμοποιείτε το PPZ, ενώ για το PPSC ο αριθμός θα παραμείνει αμετάβλητος.

Όλα όσα συζητήθηκαν παραπάνω ήταν απίθανο να είναι αποκάλυψη για κανέναν, ίσως να μην είχαμε καν κάνει πειράματα - όλοι αυτοί οι αριθμοί θα πρέπει να είναι προφανείς στους περισσότερους προγραμματιστές C++ με την πρώτη ματιά. Είναι αλήθεια ότι ένα σημείο αξίζει ακόμα διευκρίνιση: γιατί, στην περίπτωση του rvalue, το PZ δεν έχει αντίγραφο (ή άλλη κίνηση), αλλά μόνο μία κίνηση.

Λοιπόν, ρίξαμε μια ματιά στη διαφορά στη μετάδοση μεταξύ PPZ και PPSC παρατηρώντας από πρώτο χέρι τον αριθμό των αντιγράφων και των κινήσεων. Αν και είναι προφανές ότι το πλεονέκτημα του PPZ έναντι του PPSC ακόμη και σε τόσο απλά παραδείγματα είναι, για να το θέσω ήπια ΔενΠροφανώς, εξακολουθώ να κάνω, λίγο επιτηδευμένα, το εξής συμπέρασμα: εάν πρόκειται να αντιγράψουμε ακόμα το όρισμα της συνάρτησης, τότε είναι λογικό να εξετάσουμε το ενδεχόμενο να μεταβιβάσουμε το όρισμα στη συνάρτηση κατά τιμή. Γιατί έβγαλα αυτό το συμπέρασμα; Για να προχωρήσετε ομαλά στην επόμενη ενότητα.

Αν αντιγράψουμε...

Έτσι, φτάνουμε στο παροιμιώδες «αν». Τα περισσότερα από τα επιχειρήματα που συναντήσαμε δεν απαιτούσαν καθολική εφαρμογή του PPP αντί του PPSC· ζητούσαν να γίνει αυτό μόνο «αν το επιχείρημα αντιγραφεί ούτως ή άλλως». Ήρθε η ώρα να καταλάβουμε τι φταίει αυτό το επιχείρημα.

Θέλω να ξεκινήσω με μια μικρή περιγραφή του πώς γράφω κώδικα. Τον τελευταίο καιρό, η διαδικασία κωδικοποίησης μου έχει γίνει όλο και περισσότερο σαν TDD, δηλ. Η σύνταξη οποιασδήποτε μεθόδου κλάσης ξεκινά με τη σύνταξη ενός τεστ στο οποίο εμφανίζεται αυτή η μέθοδος. Κατά συνέπεια, όταν αρχίζω να γράφω ένα τεστ και δημιουργώ μια μέθοδο μετά τη συγγραφή του τεστ, εξακολουθώ να μην ξέρω αν θα αντιγράψω το όρισμα. Φυσικά, δεν δημιουργούνται όλες οι συναρτήσεις με αυτόν τον τρόπο· συχνά, ακόμη και κατά τη διαδικασία σύνταξης ενός τεστ, γνωρίζετε ακριβώς τι είδους υλοποίηση θα υπάρχει. Αλλά αυτό δεν συμβαίνει πάντα!

Κάποιος μπορεί να μου αντιταχθεί ότι δεν έχει σημασία πώς γράφτηκε αρχικά η μέθοδος, μπορούμε να αλλάξουμε τον τρόπο με τον οποίο μεταφέρουμε το επιχείρημα όταν η μέθοδος έχει διαμορφωθεί και είναι απολύτως σαφές σε εμάς τι συμβαίνει εκεί (δηλ. αν έχουμε αντιγραφή ή δεν ). Συμφωνώ εν μέρει με αυτό - πράγματι, μπορείτε να το κάνετε με αυτόν τον τρόπο, αλλά αυτό μας εμπλέκει σε κάποιο περίεργο παιχνίδι όπου πρέπει να αλλάξουμε διεπαφές μόνο και μόνο επειδή έχει αλλάξει η υλοποίηση. Κάτι που μας φέρνει στο επόμενο δίλημμα.

Αποδεικνύεται ότι τροποποιούμε (ή ακόμη σχεδιάζουμε) τη διεπαφή με βάση τον τρόπο που θα υλοποιηθεί. Δεν θεωρώ τον εαυτό μου ειδικό στο OOP και σε άλλους θεωρητικούς υπολογισμούς της αρχιτεκτονικής λογισμικού, αλλά τέτοιες ενέργειες αντιβαίνουν σαφώς στους βασικούς κανόνες όταν η υλοποίηση δεν πρέπει να επηρεάζει τη διεπαφή. Φυσικά, ορισμένες λεπτομέρειες υλοποίησης (είτε είναι χαρακτηριστικά της γλώσσας είτε της πλατφόρμας προορισμού) εξακολουθούν να διαρρέουν μέσω της διεπαφής με τον έναν ή τον άλλον τρόπο, αλλά θα πρέπει να προσπαθήσετε να μειώσετε, όχι να αυξήσετε, τον αριθμό τέτοιων πραγμάτων.

Λοιπόν, ο Θεός να τον έχει καλά, ας ακολουθήσουμε αυτό το μονοπάτι και ας αλλάξουμε τις διεπαφές ανάλογα με το τι κάνουμε στην υλοποίηση όσον αφορά την αντιγραφή του επιχειρήματος. Ας πούμε ότι γράψαμε αυτήν τη μέθοδο:

Άκυρο setName(Όνομα ονόματος) ( m_Name = move(name); )

και δεσμεύσαμε τις αλλαγές μας στο αποθετήριο. Καθώς περνούσε ο καιρός, το προϊόν λογισμικού μας απέκτησε νέα λειτουργικότητα, ενσωματώθηκαν νέα πλαίσια και προέκυψε το καθήκον να ενημερώσουμε τον έξω κόσμο για τις αλλαγές στην τάξη μας. Εκείνοι. Θα προσθέσουμε κάποιο μηχανισμό ειδοποίησης στη μέθοδό μας, ας είναι κάτι παρόμοιο με τα σήματα Qt:

Άκυρο setName(Name name) ( m_Name = move(name); emit nameChanged(m_Name); )

Υπάρχει πρόβλημα με αυτόν τον κωδικό; Τρώω. Για κάθε κλήση στο setName στέλνουμε ένα σήμα, οπότε το σήμα θα σταλεί ακόμα και όταν έννοια m_Name δεν έχει αλλάξει. Εκτός από ζητήματα απόδοσης, αυτή η κατάσταση μπορεί να οδηγήσει σε έναν άπειρο βρόχο λόγω του κώδικα που λαμβάνει την παραπάνω ειδοποίηση με κάποιο τρόπο που καλεί το setName . Για να αποφύγετε όλα αυτά τα προβλήματα, τέτοιες μέθοδοι συνήθως φαίνονται κάπως έτσι:

Άκυρο setName(Name name) ( if(name == m_Name) επιστροφή; m_Name = move(name); emit nameChanged(m_Name); )

Ξεφορτωθήκαμε τα προβλήματα που περιγράφηκαν παραπάνω, αλλά τώρα ο κανόνας μας "αν αντιγράψουμε ούτως ή άλλως..." απέτυχε - δεν υπάρχει πλέον άνευ όρων αντιγραφή του επιχειρήματος, τώρα το αντιγράφουμε μόνο εάν αλλάξει! Τι να κάνουμε λοιπόν τώρα; Αλλαγή της διεπαφής; Εντάξει, ας αλλάξουμε τη διεπαφή κλάσης εξαιτίας αυτής της επιδιόρθωσης. Τι θα γινόταν αν η τάξη μας κληρονόμησε αυτή τη μέθοδο από κάποια αφηρημένη διεπαφή; Ας το αλλάξουμε κι εκεί! Υπάρχουν πολλές αλλαγές επειδή έχει αλλάξει η εφαρμογή;

Και πάλι μπορεί να αντιταχθούν σε μένα, λένε, ο συγγραφέας, γιατί προσπαθείς να εξοικονομήσεις χρήματα σε αγώνες όταν αυτή η κατάσταση θα λειτουργήσει εκεί; Ναι, οι περισσότερες κλήσεις θα είναι ψευδείς! Υπάρχει εμπιστοσύνη σε αυτό; Οπου? Και αν αποφάσιζα να κάνω οικονομία σε αγώνες, το γεγονός ότι χρησιμοποιήσαμε PPZ δεν ήταν συνέπεια ακριβώς τέτοιων οικονομιών; Απλώς συνεχίζω την «κομματική γραμμή» που πρεσβεύει την αποτελεσματικότητα.

Κατασκευαστές

Ας δούμε εν συντομία τους κατασκευαστές, ειδικά επειδή υπάρχει ένας ειδικός κανόνας για αυτούς στο clang-tidy, ο οποίος δεν λειτουργεί ακόμη για άλλες μεθόδους/λειτουργίες. Ας υποθέσουμε ότι έχουμε μια τάξη όπως αυτή:

Κλάση JustClass ( δημόσια: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Προφανώς, η παράμετρος αντιγράφεται και το clang-tidy θα μας πει ότι θα ήταν καλή ιδέα να ξαναγράψουμε τον κατασκευαστή σε αυτό:

JustClass(string justString): m_JustString(move(justString)) ( )

Και, ειλικρινά μιλώντας, είναι δύσκολο για μένα να διαφωνήσω εδώ - τελικά, πραγματικά πάντα αντιγράφουμε. Και τις περισσότερες φορές, όταν περνάμε κάτι από έναν κατασκευαστή, το αντιγράφουμε. Αλλά πιο συχνά δεν σημαίνει πάντα. Εδώ είναι ένα άλλο παράδειγμα:

Class TimeSpan ( δημόσιο: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Εδώ δεν αντιγράφουμε πάντα, αλλά μόνο όταν οι ημερομηνίες παρουσιάζονται σωστά. Φυσικά, στη συντριπτική πλειοψηφία των περιπτώσεων αυτό θα ισχύει. Αλλά δεν είναι πάντα.

Μπορείτε να δώσετε ένα άλλο παράδειγμα, αλλά αυτή τη φορά χωρίς κωδικό. Φανταστείτε ότι έχετε μια τάξη που δέχεται ένα μεγάλο αντικείμενο. Η τάξη υπάρχει εδώ και πολύ καιρό και τώρα ήρθε η ώρα να ενημερώσετε την εφαρμογή της. Συνειδητοποιούμε ότι δεν χρειαζόμαστε περισσότερο από το ήμισυ μιας μεγάλης εγκατάστασης (η οποία έχει αυξηθεί με τα χρόνια), και ίσως ακόμη λιγότερο. Μπορούμε να κάνουμε κάτι γι' αυτό, έχοντας pass by value; Όχι, δεν θα μπορούμε να κάνουμε τίποτα, γιατί θα δημιουργηθεί ένα αντίγραφο. Αλλά αν χρησιμοποιούσαμε PPSC, απλώς θα αλλάζαμε αυτό που κάνουμε μέσασχεδιαστής. Και αυτό είναι το βασικό σημείο: χρησιμοποιώντας το PPSC ελέγχουμε τι και πότε συμβαίνει στην υλοποίηση της συνάρτησής μας (κατασκευαστής), αλλά αν χρησιμοποιήσουμε PPZ, τότε χάνουμε τον έλεγχο της αντιγραφής.

Τι μπορείτε να αφαιρέσετε από αυτήν την ενότητα; Το γεγονός ότι το επιχείρημα «αν αντιγράψουμε πάντως...» είναι πολύ αμφιλεγόμενο, γιατί Δεν ξέρουμε πάντα τι θα αντιγράψουμε, και ακόμη και όταν ξέρουμε, πολύ συχνά δεν είμαστε σίγουροι ότι αυτό θα συνεχιστεί στο μέλλον.

Η μετακόμιση είναι φθηνή

Από τη στιγμή που εμφανίστηκε η σημασιολογία της κίνησης, άρχισε να ασκεί σοβαρή επιρροή στον τρόπο με τον οποίο γράφεται ο σύγχρονος κώδικας C++, και με την πάροδο του χρόνου αυτή η επιρροή έχει μόνο ενταθεί: δεν είναι περίεργο, γιατί η κίνηση είναι τόσο φτηνόςσε σύγκριση με την αντιγραφή. Είναι όμως; Είναι αλήθεια ότι η κίνηση είναι Πάνταφτηνό χειρουργείο; Αυτό θα προσπαθήσουμε να καταλάβουμε σε αυτήν την ενότητα.

Δυαδικό Μεγάλο Αντικείμενο

Ας ξεκινήσουμε με ένα ασήμαντο παράδειγμα, ας πούμε ότι έχουμε την ακόλουθη τάξη:

Struct Blob ( std::array δεδομένα; )

Συνήθης άμορφη μάζα(BDO, αγγλικά BLOB), το οποίο μπορεί να χρησιμοποιηθεί σε διάφορες καταστάσεις. Ας δούμε τι θα μας κοστίσει να περάσουμε με αναφορά και από αξία. Το BDO μας θα χρησιμοποιηθεί κάπως έτσι:

Void Storage::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) void Storage::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

Και θα ονομάσουμε αυτές τις συναρτήσεις ως εξής:

Const Blob blob(); αποθήκευση; storage.setBlobByRef(blob); storage.setBlobByVal(blob);

Ο κώδικας για άλλα παραδείγματα θα είναι πανομοιότυπος με αυτό, μόνο με διαφορετικά ονόματα και τύπους, επομένως δεν θα τον δώσω για τα υπόλοιπα παραδείγματα - όλα βρίσκονται στο αποθετήριο.

Πριν προχωρήσουμε στις μετρήσεις, ας προσπαθήσουμε να προβλέψουμε το αποτέλεσμα. Έχουμε λοιπόν έναν πίνακα std:: 4 KB που θέλουμε να αποθηκεύσουμε σε ένα αντικείμενο κλάσης Storage. Όπως μάθαμε νωρίτερα, για το PPSC θα έχουμε ένα αντίγραφο, ενώ για το PPZ θα έχουμε ένα αντίγραφο και μία κίνηση. Με βάση το γεγονός ότι είναι αδύνατη η μετακίνηση του πίνακα, θα υπάρχουν 2 αντίγραφα για το PPZ, έναντι ενός για το PPSC. Εκείνοι. μπορούμε να περιμένουμε διπλή υπεροχή στην απόδοση για την PPSC.

Τώρα ας ρίξουμε μια ματιά στα αποτελέσματα των δοκιμών:

Αυτή και όλες οι επόμενες δοκιμές εκτελέστηκαν στο ίδιο μηχάνημα χρησιμοποιώντας το MSVS 2017 (15.7.2) και τη σημαία /O2.

Η εξάσκηση συνέπεσε με την υπόθεση - το πέρασμα από την τιμή είναι 2 φορές πιο ακριβό, γιατί για έναν πίνακα, η μετακίνηση ισοδυναμεί πλήρως με την αντιγραφή.

Γραμμή

Ας δούμε ένα άλλο παράδειγμα, ένα κανονικό std::string . Τι μπορούμε να περιμένουμε; Γνωρίζουμε (το συζήτησα αυτό στο άρθρο) ότι οι σύγχρονες υλοποιήσεις κάνουν διάκριση μεταξύ δύο τύπων συμβολοσειράς: σύντομη (περίπου 16 χαρακτήρες) και μεγάλη (αυτές που είναι μεγαλύτερες από σύντομες). Για κοντές, χρησιμοποιείται μια εσωτερική προσωρινή μνήμη, η οποία είναι μια κανονική σειρά C από char , αλλά οι μακριές θα τοποθετηθούν ήδη στο σωρό. Δεν μας ενδιαφέρουν οι μικρές γραμμές, γιατί... το αποτέλεσμα εκεί θα είναι το ίδιο με το BDO, οπότε ας επικεντρωθούμε στις μεγάλες ουρές.

Έτσι, έχοντας μια μακριά συμβολοσειρά, είναι προφανές ότι η μετακίνησή της θα πρέπει να είναι αρκετά φθηνή (απλώς μετακινήστε τον δείκτη), ώστε να μπορείτε να βασίζεστε στο γεγονός ότι η μετακίνηση της συμβολοσειράς δεν πρέπει να επηρεάζει καθόλου τα αποτελέσματα και το PPZ θα πρέπει να δώσει ένα αποτέλεσμα όχι χειρότερο από το PPSC. Ας το ελέγξουμε στην πράξη και ας έχουμε τα ακόλουθα αποτελέσματα:

Θα προχωρήσουμε στην εξήγηση αυτού του «φαινόμενου». Τι συμβαίνει λοιπόν όταν αντιγράφουμε μια υπάρχουσα συμβολοσειρά σε μια ήδη υπάρχουσα συμβολοσειρά; Ας δούμε ένα ασήμαντο παράδειγμα:

String first(64, "C"); string second(64, "N"); //... second = first;

Έχουμε δύο συμβολοσειρές 64 χαρακτήρων, επομένως το εσωτερικό buffer είναι ανεπαρκές κατά τη δημιουργία τους, με αποτέλεσμα και οι δύο συμβολοσειρές να εκχωρούνται στο σωρό. Τώρα αντιγράφουμε από το πρώτο στο δεύτερο. Επειδή Τα μεγέθη των σειρών μας είναι τα ίδια, προφανώς υπάρχει αρκετός χώρος στη δεύτερη για να χωρέσει όλα τα δεδομένα από την πρώτη, άρα δεύτερη = πρώτη. θα είναι ένα κοινόχρηστο memcpy, τίποτα περισσότερο. Αλλά αν δούμε ένα ελαφρώς τροποποιημένο παράδειγμα:

String first(64, "C"); string second = first;

τότε δεν θα υπάρχει πλέον κλήση στον operator= , αλλά θα κληθεί ο κατασκευαστής αντιγραφής. Επειδή Εφόσον έχουμε να κάνουμε με έναν κατασκευαστή, δεν υπάρχει καμία υπάρχουσα μνήμη σε αυτόν. Πρέπει πρώτα να επιλεγεί και μόνο μετά να αντιγραφεί πρώτα. Εκείνοι. αυτή είναι η εκχώρηση μνήμης και μετά το memcpy. Όπως γνωρίζετε και εσείς, η εκχώρηση μνήμης στον παγκόσμιο σωρό είναι συνήθως μια δαπανηρή λειτουργία, επομένως η αντιγραφή από το δεύτερο παράδειγμα θα είναι πιο ακριβή από την αντιγραφή από το πρώτο. Πιο ακριβή κατανομή μνήμης ανά σωρό.

Τι σχέση έχει αυτό με το θέμα μας; Το πιο άμεσο, γιατί το πρώτο παράδειγμα δείχνει ακριβώς τι συμβαίνει με το PPSC και το δεύτερο δείχνει τι συμβαίνει με το PPZ: για το PPZ δημιουργείται πάντα μια νέα σειρά, ενώ για το PPSC η υπάρχουσα επαναχρησιμοποιείται. Έχετε ήδη δει τη διαφορά στον χρόνο εκτέλεσης, επομένως δεν υπάρχει τίποτα να προσθέσετε εδώ.

Και εδώ βρισκόμαστε αντιμέτωποι με το γεγονός ότι όταν χρησιμοποιούμε τη ΣΔΙΤ, εργαζόμαστε εκτός πλαισίου και επομένως δεν μπορούμε να χρησιμοποιήσουμε όλα τα οφέλη που μπορεί να προσφέρει. Και αν προηγουμένως συλλογιστήκαμε ως προς τις θεωρητικές μελλοντικές αλλαγές, εδώ παρατηρούμε μια πολύ συγκεκριμένη αποτυχία στην παραγωγικότητα.

Φυσικά, κάποιος θα μπορούσε να μου αντιταχθεί ότι η συμβολοσειρά ξεχωρίζει και οι περισσότεροι τύποι δεν λειτουργούν με αυτόν τον τρόπο. Στο οποίο μπορώ να απαντήσω το εξής: όλα όσα περιγράφηκαν νωρίτερα θα ισχύουν για οποιοδήποτε κοντέινερ που εκχωρεί μνήμη στο σωρό αμέσως για ένα πακέτο στοιχείων. Επίσης, ποιος ξέρει ποιες άλλες βελτιστοποιήσεις με ευαισθησία στο περιβάλλον χρησιμοποιούνται σε άλλους τύπους;

Τι πρέπει να αφαιρέσετε από αυτήν την ενότητα; Το γεγονός ότι ακόμα κι αν η μετακίνηση είναι πραγματικά φθηνή δεν σημαίνει ότι η αντικατάσταση της αντιγραφής με αντιγραφή+μετακίνηση θα δίνει πάντα ένα αποτέλεσμα συγκρίσιμο από άποψη απόδοσης.

Σύνθετος τύπος

Τέλος, ας δούμε έναν τύπο που θα αποτελείται από πολλά αντικείμενα. Ας είναι αυτή η κλάση Person, η οποία αποτελείται από δεδομένα που είναι εγγενή σε ένα άτομο. Συνήθως αυτό είναι το όνομα, το επίθετό σας, ο ταχυδρομικός κώδικας κ.λπ. Μπορείτε να αναπαραστήσετε όλα αυτά ως συμβολοσειρές και να υποθέσετε ότι οι συμβολοσειρές που βάζετε στα πεδία της κλάσης Person είναι πιθανό να είναι σύντομες. Αν και πιστεύω ότι στην πραγματική ζωή, η μέτρηση κοντών χορδών θα είναι πιο χρήσιμη, θα συνεχίσουμε να εξετάζουμε χορδές διαφορετικών μεγεθών για να δώσουμε μια πιο ολοκληρωμένη εικόνα.

Θα χρησιμοποιήσω επίσης το πρόσωπο με 10 πεδία, αλλά για αυτό δεν θα δημιουργήσω 10 πεδία απευθείας στο σώμα της τάξης. Η υλοποίηση του Person κρύβει ένα κοντέινερ στα βάθη του - αυτό καθιστά πιο βολική την αλλαγή των παραμέτρων δοκιμής, πρακτικά χωρίς να αποκλίνουμε από το πώς θα λειτουργούσε αν το άτομο ήταν πραγματική τάξη. Ωστόσο, η υλοποίηση είναι διαθέσιμη και μπορείτε πάντα να ελέγξετε τον κωδικό και να μου πείτε αν έκανα κάτι λάθος.

Λοιπόν, πάμε: Άτομο με 10 πεδία τύπου string , τα οποία μεταφέρουμε χρησιμοποιώντας PPSC και PPZ στο Storage:

Όπως μπορείτε να δείτε, έχουμε μια τεράστια διαφορά στην απόδοση, η οποία δεν πρέπει να αποτελεί έκπληξη στους αναγνώστες μετά τις προηγούμενες ενότητες. Πιστεύω επίσης ότι η κλάση Person είναι αρκετά "πραγματική" ώστε τέτοια αποτελέσματα δεν θα παραβλεφθούν ως αφηρημένα.

Παρεμπιπτόντως, όταν ετοίμαζα αυτό το άρθρο, ετοίμασα ένα άλλο παράδειγμα: μια κλάση που χρησιμοποιεί πολλά αντικείμενα std::function. Σύμφωνα με την ιδέα μου, υποτίθεται ότι θα έδειχνε επίσης πλεονέκτημα στην απόδοση της PPSC έναντι της PPZ, αλλά αποδείχθηκε ακριβώς το αντίθετο! Αλλά δεν δίνω αυτό το παράδειγμα εδώ όχι επειδή δεν μου άρεσαν τα αποτελέσματα, αλλά επειδή δεν είχα χρόνο να καταλάβω γιατί προέκυψαν τέτοια αποτελέσματα. Παρόλα αυτά, υπάρχει κώδικας στο αποθετήριο (Εκτυπωτές), δοκιμές - επίσης, αν κάποιος θέλει να το καταλάβει, θα χαρώ να ακούσω για τα αποτελέσματα της έρευνας. Σκοπεύω να επιστρέψω σε αυτό το παράδειγμα αργότερα, και αν κανείς δεν δημοσιεύσει αυτά τα αποτελέσματα πριν από εμένα, τότε θα τα εξετάσω σε ξεχωριστό άρθρο.

Αποτελέσματα

Εξετάσαμε λοιπόν τα διάφορα πλεονεκτήματα και μειονεκτήματα του να περάσουμε από την τιμή και να περάσουμε με αναφορά σε μια σταθερά. Εξετάσαμε μερικά παραδείγματα και εξετάσαμε την απόδοση και των δύο μεθόδων σε αυτά τα παραδείγματα. Φυσικά, αυτό το άρθρο δεν μπορεί και δεν είναι εξαντλητικό, αλλά, κατά τη γνώμη μου, περιέχει αρκετές πληροφορίες για να λάβετε μια ανεξάρτητη και τεκμηριωμένη απόφαση σχετικά με τη μέθοδο που είναι καλύτερο να χρησιμοποιήσετε. Κάποιος μπορεί να αντιταχθεί: "γιατί να χρησιμοποιήσετε μία μέθοδο, ας ξεκινήσουμε από την εργασία!" Ενώ συμφωνώ με αυτή τη διατριβή γενικά, διαφωνώ μαζί της σε αυτήν την περίπτωση. Πιστεύω ότι μπορεί να υπάρχει μόνο ένας τρόπος να περάσουν επιχειρήματα σε μια γλώσσα, που χρησιμοποιείται από προεπιλογή.

Τι σημαίνει προεπιλογή; Αυτό σημαίνει ότι όταν γράφω μια συνάρτηση, δεν σκέφτομαι πώς πρέπει να μεταφέρω το όρισμα, απλώς χρησιμοποιώ το "default". Η γλώσσα C++ είναι μια αρκετά περίπλοκη γλώσσα που πολλοί άνθρωποι αποφεύγουν. Και κατά τη γνώμη μου, η πολυπλοκότητα προκαλείται όχι τόσο από την πολυπλοκότητα των γλωσσικών δομών που υπάρχουν στη γλώσσα (ένας τυπικός προγραμματιστής μπορεί να μην τις συναντήσει ποτέ), αλλά από το γεγονός ότι η γλώσσα σε κάνει να σκέφτεσαι πολύ: έχω ελευθερώσει ανεβάσετε τη μνήμη, είναι ακριβό να χρησιμοποιήσετε αυτήν τη λειτουργία εδώ; και ούτω καθεξής.

Πολλοί προγραμματιστές (C, C++ και άλλοι) είναι δύσπιστοι και φοβούνται την C++ που άρχισε να εμφανίζεται μετά το 2011. Έχω ακούσει πολλές κριτικές ότι η γλώσσα γίνεται πιο σύνθετη, ότι μόνο «γκουρού» μπορούν πλέον να γράφουν σε αυτήν κ.λπ. Προσωπικά, πιστεύω ότι αυτό δεν είναι έτσι - αντίθετα, η επιτροπή αφιερώνει πολύ χρόνο στο να κάνει τη γλώσσα πιο φιλική στους αρχάριους και ώστε οι προγραμματιστές να χρειάζεται να σκέφτονται λιγότερο τα χαρακτηριστικά της γλώσσας. Εξάλλου, αν δεν χρειάζεται να παλέψουμε με τη γλώσσα, τότε έχουμε χρόνο να σκεφτούμε το έργο. Αυτές οι απλοποιήσεις περιλαμβάνουν έξυπνους δείκτες, συναρτήσεις λάμδα και πολλά άλλα που εμφανίστηκαν στη γλώσσα. Ταυτόχρονα, δεν αρνούμαι το γεγονός ότι τώρα πρέπει να μελετάμε περισσότερο, αλλά τι φταίει να σπουδάζουμε; Ή μήπως δεν συμβαίνουν αλλαγές σε άλλες δημοφιλείς γλώσσες που πρέπει να μάθουμε;

Επιπλέον, δεν έχω καμία αμφιβολία ότι θα υπάρξουν σνομπ που θα μπορούν να πουν ως απάντηση: «Δεν θέλεις να σκέφτεσαι; Μετά πηγαίνετε να γράψετε σε PHP." Δεν θέλω καν να απαντήσω σε τέτοιους ανθρώπους. Θα δώσω μόνο ένα παράδειγμα από την πραγματικότητα του παιχνιδιού: στο πρώτο μέρος του Starcraft, όταν ένας νέος εργάτης δημιουργείται σε ένα κτίριο, για να αρχίσει να εξορύσσει ορυκτά (ή αέριο), έπρεπε να σταλεί χειροκίνητα εκεί. Επιπλέον, κάθε πακέτο ορυκτών είχε ένα όριο, στο οποίο η αύξηση των εργαζομένων ήταν άχρηστη, και μπορούσαν ακόμη και να παρεμβαίνουν μεταξύ τους, επιδεινώνοντας την παραγωγή. Αυτό άλλαξε στο Starcraft 2: οι εργαζόμενοι αρχίζουν αυτόματα να εξορύσσουν ορυκτά (ή φυσικό αέριο) και υποδεικνύει επίσης πόσοι εργαζόμενοι εξορύσσουν αυτήν τη στιγμή και πόσο είναι το όριο αυτού του κοιτάσματος. Αυτό απλοποίησε πολύ την αλληλεπίδραση του παίκτη με τη βάση, επιτρέποντάς του να επικεντρωθεί σε πιο σημαντικές πτυχές του παιχνιδιού: οικοδόμηση βάσης, συσσώρευση στρατευμάτων και καταστροφή του εχθρού. Φαίνεται ότι πρόκειται απλώς για μια μεγάλη καινοτομία, αλλά αυτό που ξεκίνησε στο Διαδίκτυο! Οι άνθρωποι (ποιοι είναι αυτοί;) άρχισαν να ουρλιάζουν ότι το παιχνίδι «σκοτώθηκε» και «σκότωσαν το Starcraft». Προφανώς, τέτοια μηνύματα θα μπορούσαν να προέρχονται μόνο από «φυλάκτες μυστικής γνώσης» και «ειδικούς του υψηλού APM» που τους άρεσε να βρίσκονται σε κάποια «ελίτ» λέσχη.

Επιστρέφοντας λοιπόν στο θέμα μας, όσο λιγότερο χρειάζεται να σκέφτομαι πώς να γράφω κώδικα, τόσο περισσότερο χρόνο έχω να σκεφτώ να λύσω το άμεσο πρόβλημα. Το να σκέφτομαι ποια μέθοδο πρέπει να χρησιμοποιήσω - PPSC ή PPZ - δεν με φέρνει ούτε ένα γιώτα πιο κοντά στην επίλυση του προβλήματος, επομένως απλώς αρνούμαι να σκεφτώ τέτοια πράγματα και επιλέγω μία επιλογή: να περάσω με αναφορά σε μια σταθερά. Γιατί; Επειδή δεν βλέπω κανένα πλεονέκτημα για τις ΣΔΙΤ σε γενικές περιπτώσεις, και οι ειδικές περιπτώσεις πρέπει να εξετάζονται χωριστά.

Είναι μια ειδική περίπτωση, απλά, έχοντας παρατηρήσει ότι σε κάποια μέθοδο το PPSC αποδεικνύεται συμφόρηση και αλλάζοντας τη μετάδοση στο PPZ, θα έχουμε σημαντική αύξηση στην απόδοση, δεν διστάζω να χρησιμοποιήσω το PPZ. Αλλά από προεπιλογή, θα χρησιμοποιήσω το PPSC τόσο σε κανονικές λειτουργίες όσο και σε κατασκευαστές. Και αν είναι δυνατόν, θα προωθήσω τη συγκεκριμένη μέθοδο όπου είναι δυνατόν. Γιατί; Επειδή πιστεύω ότι η πρακτική της προώθησης των ΣΔΙΤ είναι μοχθηρή λόγω του γεγονότος ότι η μερίδα του λέοντος των προγραμματιστών δεν είναι πολύ ενημερωμένοι (είτε κατ' αρχήν, είτε απλώς δεν έχουν μπει ακόμα στην ταλάντευση των πραγμάτων) και απλώς ακολουθούν τις συμβουλές. Επιπλέον, αν υπάρχουν πολλές αντικρουόμενες συμβουλές, επιλέγουν αυτή που είναι πιο απλή, και αυτό οδηγεί σε απαισιοδοξία στον κώδικα απλώς και μόνο επειδή κάποιος κάπου άκουσε κάτι. Ω ναι, αυτός ο κάποιος μπορεί επίσης να παρέχει έναν σύνδεσμο προς το άρθρο του Abrahams για να αποδείξει ότι έχει δίκιο. Και μετά κάθεσαι, διαβάζεις τον κώδικα και σκέφτεσαι: είναι το γεγονός ότι η παράμετρος μεταβιβάζεται με τιμή εδώ επειδή ο προγραμματιστής που το έγραψε αυτό προήλθε από την Java, απλά διάβασε πολλά "έξυπνα" άρθρα ή υπάρχει πραγματικά ανάγκη για ένα τεχνικές προδιαγραφές?

Το PPSC διαβάζεται πολύ πιο εύκολα: το άτομο γνωρίζει ξεκάθαρα την «καλή μορφή» της C++ και προχωράμε - το βλέμμα δεν καθυστερεί. Η πρακτική της χρήσης PPSC διδάσκεται στους προγραμματιστές της C++ εδώ και χρόνια, ποιος είναι ο λόγος να την εγκαταλείψουμε; Αυτό με οδηγεί σε ένα άλλο συμπέρασμα: εάν μια διεπαφή μεθόδου χρησιμοποιεί PPP, τότε θα πρέπει επίσης να υπάρχει ένα σχόλιο γιατί συμβαίνει αυτό. Σε άλλες περιπτώσεις θα πρέπει να εφαρμόζεται το PPSC. Φυσικά, υπάρχουν τύποι εξαίρεσης, αλλά δεν τους αναφέρω εδώ απλώς επειδή υπονοούνται: string_view , initializer_list , διάφοροι επαναλήπτες κ.λπ. Αλλά αυτές είναι εξαιρέσεις, ο κατάλογος των οποίων μπορεί να επεκταθεί ανάλογα με τους τύπους που χρησιμοποιούνται στο έργο. Αλλά η ουσία παραμένει η ίδια από την C++98: από προεπιλογή χρησιμοποιούμε πάντα PPCS.

Για std::string πιθανότατα δεν θα υπάρχει διαφορά στις μικρές χορδές, θα μιλήσουμε για αυτό αργότερα.

Ζητώ εκ των προτέρων συγγνώμη για τον επιτηδευμένο σχολιασμό σχετικά με την "τοποθέτηση πόντων", αλλά πρέπει με κάποιο τρόπο να σας παρασύρουμε στο άρθρο)) Από την πλευρά μου, θα προσπαθήσω να διασφαλίσω ότι η περίληψη εξακολουθεί να ανταποκρίνεται στις προσδοκίες σας.

Συνοπτικά για τι μιλάμε

Όλοι το γνωρίζουν ήδη αυτό, αλλά στην αρχή θα σας υπενθυμίσω πώς μπορούν να περάσουν οι παράμετροι της μεθόδου στο 1C. Μπορούν να περαστούν "κατά αναφορά" ή "κατά τιμή". Στην πρώτη περίπτωση, περνάμε στη μέθοδο την ίδια τιμή με το σημείο κλήσης και στη δεύτερη ένα αντίγραφό της.

Από προεπιλογή, στο 1C, τα ορίσματα μεταβιβάζονται με αναφορά και οι αλλαγές σε μια παράμετρο μέσα σε μια μέθοδο θα είναι ορατές εκτός της μεθόδου. Εδώ, η περαιτέρω κατανόηση της ερώτησης εξαρτάται από το τι ακριβώς καταλαβαίνετε με τη λέξη "αλλαγή παραμέτρου". Άρα, αυτό σημαίνει εκ νέου ανάθεση και τίποτα περισσότερο. Επιπλέον, η εκχώρηση μπορεί να είναι σιωπηρή, για παράδειγμα, η κλήση μιας μεθόδου πλατφόρμας που επιστρέφει κάτι στην παράμετρο εξόδου.

Αλλά αν δεν θέλουμε η παράμετρός μας να μεταβιβάζεται με αναφορά, τότε μπορούμε να καθορίσουμε μια λέξη-κλειδί πριν από την παράμετρο Εννοια

Διαδικασία ByValue(Value Parameter) Παράμετρος = 2; Παράμετρος EndProcedure = 1; ByValue(Παράμετρος); Αναφορά (Παράμετρος); // θα εκτυπώσει 1

Όλα λειτουργούν όπως υποσχέθηκαν - η αλλαγή (ή μάλλον "αντικατάσταση") της τιμής της παραμέτρου δεν αλλάζει την τιμή εκτός της μεθόδου.

Λοιπόν, ποιο είναι το αστείο;

Οι ενδιαφέρουσες στιγμές ξεκινούν όταν αρχίζουμε να περνάμε όχι πρωτόγονους τύπους (συμβολοσειρές, αριθμούς, ημερομηνίες κ.λπ.) ως παραμέτρους, αλλά αντικείμενα. Εδώ μπαίνουν στο παιχνίδι έννοιες όπως «ρηχά» και «βαθιά» αντίγραφα ενός αντικειμένου, καθώς και δείκτες (όχι με όρους C++, αλλά ως αφηρημένες λαβές).

Όταν μεταβιβάζουμε ένα αντικείμενο (για παράδειγμα, έναν Πίνακα τιμών) με αναφορά, περνάμε την ίδια την τιμή του δείκτη (μια συγκεκριμένη λαβή), η οποία «κρατά» το αντικείμενο στη μνήμη της πλατφόρμας. Όταν περάσει η τιμή, η πλατφόρμα θα δημιουργήσει ένα αντίγραφο αυτού του δείκτη.

Με άλλα λόγια, εάν, μεταβιβάζοντας ένα αντικείμενο με αναφορά, σε μια μέθοδο αποδώσουμε την τιμή “Array” στην παράμετρο, τότε στο σημείο κλήσης θα λάβουμε έναν πίνακα. Η εκ νέου εκχώρηση της τιμής που μεταβιβάστηκε με αναφορά είναι ορατή από την τοποθεσία κλήσης.

Διαδικασία ProcessValue(Parameter) Parameter = New Array; EndProcedure Table = New ValueTable; ProcessValue(Πίνακας); Report(ValueType(Table)); // θα παράγει έναν πίνακα

Αν περάσουμε το αντικείμενο με τιμή, τότε στο σημείο κλήσης ο Πίνακας Τιμών μας δεν θα χαθεί.

Περιεχόμενα και κατάσταση αντικειμένου

Κατά τη μετάβαση από την τιμή, δεν αντιγράφεται ολόκληρο το αντικείμενο, αλλά μόνο ο δείκτης του. Το παράδειγμα αντικειμένου παραμένει το ίδιο. Δεν έχει σημασία πώς περνάτε το αντικείμενο, με αναφορά ή με τιμή - η εκκαθάριση του πίνακα τιμών θα διαγράψει τον ίδιο τον πίνακα. Αυτός ο καθαρισμός θα είναι ορατός παντού, γιατί... υπήρχε μόνο ένα αντικείμενο και δεν είχε σημασία πόσο ακριβώς μεταβιβάστηκε στη μέθοδο.

Διαδικασία ProcessValue(Parameter) Parameter.Clear(); EndProcedure Table = New ValueTable; Table.Add(); ProcessValue(Πίνακας); Αναφορά(Table.Quantity()); // θα βγει 0

Κατά τη μεταβίβαση αντικειμένων σε μεθόδους, η πλατφόρμα λειτουργεί με δείκτες (υπό όρους, όχι άμεσα ανάλογα από τη C++). Εάν ένα αντικείμενο μεταβιβαστεί με αναφορά, τότε το κελί μνήμης της εικονικής μηχανής 1C στην οποία βρίσκεται το αντικείμενο μπορεί να αντικατασταθεί από άλλο αντικείμενο. Εάν ένα αντικείμενο μεταβιβαστεί με τιμή, τότε ο δείκτης αντιγράφεται και η αντικατάσταση του αντικειμένου δεν έχει ως αποτέλεσμα την αντικατάσταση της θέσης μνήμης με το αρχικό αντικείμενο.

Ταυτόχρονα, οποιαδήποτε αλλαγή κατάστασηαντικείμενο (καθαρισμός, προσθήκη ιδιοτήτων κ.λπ.) αλλάζει το ίδιο το αντικείμενο και δεν έχει καμία απολύτως σχέση με το πώς και πού μεταφέρθηκε το αντικείμενο. Η κατάσταση ενός στιγμιότυπου αντικειμένου έχει αλλάξει· μπορεί να υπάρχει ένα σωρό «αναφορές» και «τιμές» σε αυτό, αλλά το παράδειγμα είναι πάντα το ίδιο. Περνώντας ένα αντικείμενο σε μια μέθοδο, δεν δημιουργούμε αντίγραφο ολόκληρου του αντικειμένου.

Και αυτό ισχύει πάντα, εκτός από...

Αλληλεπίδραση πελάτη-διακομιστή

Η πλατφόρμα υλοποιεί κλήσεις διακομιστή με μεγάλη διαφάνεια. Απλώς καλούμε μια μέθοδο και κάτω από την κουκούλα η πλατφόρμα σειριοποιεί (μετατρέπει σε συμβολοσειρά) όλες τις παραμέτρους της μεθόδου, τις μεταβιβάζει στον διακομιστή και, στη συνέχεια, επιστρέφει τις παραμέτρους εξόδου πίσω στον πελάτη, όπου αποσυντονίζονται και ζουν ως αν δεν είχαν πάει ποτέ σε κανέναν διακομιστή.

Όπως γνωρίζετε, δεν είναι όλα τα αντικείμενα πλατφόρμας σειριοποιήσιμα. Εδώ μεγαλώνει ο περιορισμός: δεν μπορούν να περάσουν όλα τα αντικείμενα στη μέθοδο διακομιστή από τον πελάτη. Εάν περάσετε ένα μη σειριοποιήσιμο αντικείμενο, η πλατφόρμα θα αρχίσει να χρησιμοποιεί κακές λέξεις.

  • Ρητή δήλωση των προθέσεων του προγραμματιστή. Εξετάζοντας την υπογραφή της μεθόδου, μπορείτε να πείτε με σαφήνεια ποιες παράμετροι εισάγονται και ποιες εξάγονται. Αυτός ο κώδικας διαβάζεται και διατηρείται ευκολότερα
  • Για να είναι ορατή μια αλλαγή στην παράμετρο «by reference» στο διακομιστή στο σημείο κλήσης στον πελάτη, pΗ ίδια η πλατφόρμα θα επιστρέψει αναγκαστικά τις παραμέτρους που μεταβιβάστηκαν στον διακομιστή μέσω συνδέσμου στον πελάτη, προκειμένου να διασφαλιστεί η συμπεριφορά που περιγράφεται στην αρχή του άρθρου. Εάν η παράμετρος δεν χρειάζεται να επιστραφεί, θα υπάρξει υπέρβαση κυκλοφορίας. Για τη βελτιστοποίηση της ανταλλαγής δεδομένων, οι παράμετροι των οποίων τις τιμές δεν χρειαζόμαστε στην έξοδο θα πρέπει να επισημαίνονται με τη λέξη Value.

Το δεύτερο σημείο είναι αξιοσημείωτο εδώ. Για τη βελτιστοποίηση της επισκεψιμότητας, η πλατφόρμα δεν θα επιστρέψει την τιμή της παραμέτρου στον πελάτη, εάν η παράμετρος επισημαίνεται με τη λέξη Τιμή. Όλα αυτά είναι υπέροχα, αλλά οδηγούν σε ένα ενδιαφέρον αποτέλεσμα.

Όπως είπα ήδη, όταν ένα αντικείμενο μεταφέρεται στον διακομιστή, λαμβάνει χώρα η σειριοποίηση, δηλ. εκτελείται ένα «βαθύ» αντίγραφο του αντικειμένου. Κι αν υπάρχει λέξη Εννοιατο αντικείμενο δεν θα ταξιδέψει από τον διακομιστή πίσω στον πελάτη. Προσθέτουμε αυτά τα δύο γεγονότα και παίρνουμε τα εξής:

&OnServerProcedureByLink(Parameter) Parameter.Clear(); EndProcedure &OnServerProcedureByValue(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure ByValueClient(Value Parameter) Parameter.Clear(); EndProcedure &OnClient Procedure CheckValue() List1= New ListValues; List1.Add("γεια"); List2 = List1.Copy(); List3 = List1.Copy(); // το αντικείμενο αντιγράφεται πλήρως, // μεταφέρεται στον διακομιστή και μετά επιστρέφεται. // Η εκκαθάριση της λίστας είναι ορατή στο σημείο κλήσης ByRef(List1); // το αντικείμενο αντιγράφεται πλήρως, // μεταφέρεται στον διακομιστή. Δεν επιστρέφει. // Η εκκαθάριση της λίστας ΔΕΝ είναι ΟΡΑΤΗ στο σημείο κλήσης ByValue(List2). // αντιγράφεται μόνο ο δείκτης αντικειμένου // η εκκαθάριση της λίστας είναι ορατή στο σημείο της κλήσης προς το ByValueClient(List3); Αναφορά(List1.Quantity()); Αναφορά(List2.Quantity()); Αναφορά(List3.Quantity()); Τέλος Διαδικασίας

Περίληψη

Συνοπτικά, μπορεί να συνοψιστεί ως εξής:

  • Η διέλευση μέσω αναφοράς σάς επιτρέπει να "αντικαταστήσετε" ένα αντικείμενο με ένα εντελώς διαφορετικό αντικείμενο
  • Η μετάβαση από την τιμή δεν σας επιτρέπει να "αντικαταστήσετε" το αντικείμενο, αλλά οι αλλαγές στην εσωτερική κατάσταση του αντικειμένου θα είναι ορατές, επειδή εργαζόμαστε με το ίδιο παράδειγμα αντικειμένου
  • Κατά την πραγματοποίηση κλήσης διακομιστή, η εργασία γίνεται με ΔΙΑΦΟΡΕΤΙΚΑ στιγμιότυπα του αντικειμένου, επειδή Έγινε ένα βαθύ αντίγραφο. Λέξη-κλειδί Εννοιαθα αποτρέψει την αντιγραφή της παρουσίας διακομιστή στην παρουσία πελάτη και η αλλαγή της εσωτερικής κατάστασης ενός αντικειμένου στον διακομιστή δεν θα οδηγήσει σε παρόμοια αλλαγή στον υπολογιστή-πελάτη.

Ελπίζω ότι αυτή η απλή λίστα κανόνων θα σας διευκολύνει να επιλύσετε διαφορές με συναδέλφους σχετικά με τη μετάδοση παραμέτρων "κατά τιμή" και "κατά αναφορά"