Pourquoi les listes liées utilisent-elles des pointeurs au lieu de stocker des nœuds à l'intérieur des nœuds?


121

J'ai déjà beaucoup travaillé avec des listes liées en Java, mais je suis très novice en C ++. J'utilisais cette classe de nœuds qui m'a été donnée dans un projet très bien

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

mais j'avais une question qui n'a pas été très bien répondue. Pourquoi est-il nécessaire d'utiliser

Node *m_next;

pour pointer vers le nœud suivant de la liste au lieu de

Node m_next;

Je comprends qu'il vaut mieux utiliser la version pointeur; Je ne vais pas discuter des faits, mais je ne sais pas pourquoi c'est mieux. J'ai eu une réponse pas si claire sur la façon dont le pointeur est meilleur pour l'allocation de mémoire, et je me demandais si quelqu'un ici pourrait m'aider à mieux comprendre cela.


14
@self Pardonnez-moi? Pourquoi une langue où tout est un pointeur n'aurait-elle pas de liste chaînée?
Angew n'est plus fier de SO

41
Il est important de noter en quoi C et C ++ sont distincts de Java en termes de pointeurs d'objet par rapport aux références. Node m_nextn'est pas une référence à un nœud, c'est un stockage pour l'ensemble Nodelui-même.
Brian Cain

41
@self Java a des pointeurs que vous ne les utilisez pas explicitement.
m0meni

27
Les tortues tout en bas ne sont pas une option. La folie doit s'arrêter quelque part.
WhozCraig

26
Veuillez oublier tout ce que vous savez sur Java. C ++ et Java gèrent la mémoire de manière fondamentalement différente. Allez voir cette question pour des recommandations de livres, choisissez-en une et lisez-la. Vous nous rendrez à tous une immense faveur.
Rob K

Réponses:


218

Ce n'est pas seulement mieux, c'est le seul moyen possible.

Si vous stockiez un Node objet à l' intérieur de lui-même, que serait- sizeof(Node)il? Ce serait sizeof(int) + sizeof(Node), qui serait égal à sizeof(int) + (sizeof(int) + sizeof(Node)), qui serait égal àsizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node))) , etc. à l'infini.

Un objet comme celui-là ne peut pas exister. C'est impossible .


25
* Sauf s'il est évalué paresseusement. Des listes infinies sont possibles, mais pas avec une évaluation stricte.
Carcigenicate

55
@Carcigenicate il ne s'agit pas d'évaluer / d'exécuter une fonction sur l'objet Node - il s'agit de la disposition de la mémoire de chaque instance de Node, qui doit être déterminée au moment de la compilation, avant qu'une évaluation puisse avoir lieu.
Peteris

6
@DavidK C'est logiquement impossible de faire ça. Vous avez besoin d' un pointeur (enfin vraiment une indirection) ici - bien sûr que la langue peut vous le cacher, mais à la fin, pas moyen de contourner cela.
Voo le

2
@David Je suis confus. D'abord vous convenez que c'est logiquement impossible, mais ensuite vous voulez le contempler? Supprimez tout ce qui est C ou C ++ - c'est impossible dans n'importe quel langage que vous pourriez imaginer pour autant que je puisse voir. Cette structure est par définition une récursion infinie et sans un certain niveau d'indirection, nous ne pouvons pas rompre cela.
Voo

13
@benjamin J'ai en fait fait remarquer (parce que je savais sinon que quelqu'un soulèverait cela - et bien n'a pas aidé) que Haskell a alloué les thunks au moment de la création et donc cela fonctionne parce que ces thunks nous donnent l'indirection dont nous avons besoin. Ce n'est rien d'autre qu'un pointeur avec des données supplémentaires déguisées ...
Voo

178

En Java

Node m_node

stocke un pointeur vers un autre nœud. Vous n'avez pas le choix. En C ++

Node *m_node

signifie la même chose. La différence est qu'en C ++, vous pouvez en fait stocker l'objet plutôt qu'un pointeur vers celui-ci. C'est pourquoi vous devez dire que vous voulez un pointeur. En C ++:

Node m_node

signifie stocker le nœud ici (et cela ne peut clairement pas fonctionner pour une liste - vous vous retrouvez avec une structure définie de manière récursive).


2
@SalmanA Je le savais déjà. Je voulais juste savoir pourquoi cela ne fonctionnerait pas sans un pointeur, ce que la réponse acceptée expliquait beaucoup mieux.
m0meni

3
@ AR7 Ils donnent tous les deux la même explication, juste sous deux approches différentes. Si vous la déclariez comme une variable "régulière", la première fois qu'un constructeur était appelé, il l'instancierait dans une nouvelle instance. Mais avant qu'il ne finisse de l'instancier - avant que le constructeur du premier one ne soit terminé - le Nodepropre constructeur du membre serait appelé, ce qui instancierait une autre nouvelle instance ... et vous obtiendriez une pseudo-récursion sans fin. Ce n'est pas vraiment un problème de taille en termes complètement stricts et littéraux, mais un problème de performances.
Panzercrisis

Mais tout ce que vous voulez vraiment, c'est juste un moyen de pointer vers celui qui est le suivant dans la liste, pas Nodecelui qui est en fait dans le premier Node. Vous créez donc un pointeur, qui est essentiellement la manière dont Java gère les objets, par opposition aux primitives. Lorsque vous appelez une méthode ou créez une variable, Java ne stocke pas une copie de l'objet ni même l'objet lui-même; il stocke une référence à un objet, qui est essentiellement un pointeur avec un petit gant d'enfant enroulé autour de lui. C'est ce que disent essentiellement les deux réponses.
Panzercrisis

ce n'est pas un problème de taille ou de vitesse - c'est un problème d'impossibilité. L'objet Node inclus inclurait un objet Node qui inclurait un objet Node ... En fait, il est impossible de le compiler
pm100

3
@Panzercrisis Je sais qu'ils donnent tous les deux la même explication. Cette approche, cependant, ne m'a pas été aussi utile car elle se concentrait sur ce que j'avais déjà compris: comment les pointeurs fonctionnent en C ++ et comment les pointeurs sont gérés en Java. La réponse acceptée expliquait spécifiquement pourquoi ne pas utiliser de pointeur serait impossible car la taille ne peut pas être calculée. En revanche, celle-ci l'a laissé plus vaguement comme «une structure définie récursivement». PS votre explication que vous venez d'écrire l'explique mieux que les deux: D.
m0meni

38

C ++ n'est pas Java. Quand tu écris

Node m_next;

en Java, cela revient à écrire

Node* m_next;

en C ++. En Java, le pointeur est implicite, en C ++, il est explicite. Si vous écrivez

Node m_next;

en C ++, vous placez une instance de Nodelà à l'intérieur de l'objet que vous définissez. Il est toujours là et ne peut pas être omis, il ne peut pas être attribué avec newet il ne peut pas être supprimé. Cet effet est impossible à obtenir en Java, et il est totalement différent de ce que fait Java avec la même syntaxe.


1
Obtenir quelque chose de similaire en Java serait probablement "étend" si SuperNode étend Node, SuperNodes inclut tous les attributs de Node et doit réserver tout l'espace supplémentaire. Donc en Java, vous ne pouvez pas faire "Node extend Node"
Falco

@Falco Vrai, l'héritage est une forme d'inclusion sur place des classes de base. Cependant, étant donné que Java ne permet pas l'héritage multiple (contrairement à C ++), vous ne pouvez extraire qu'une instance d'une seule autre classe préexistante via l'héritage. C'est pourquoi je ne considérerais pas l'héritage comme un substitut à l'inclusion des membres en place.
cmaster - réintégrer monica le

27

Vous utilisez un pointeur, sinon votre code:

class Node
{
   //etc
   Node m_next; //non-pointer
};

Ne compilerait pas , car le compilateur ne peut pas calculer la taille de Node. C'est parce que cela dépend de lui-même - ce qui signifie que le compilateur ne peut pas décider de la quantité de mémoire qu'il consommerait.


5
Pire encore, aucune taille valide n'existe: si k == sizeof(Node)tient et que votre type contient des données, il devrait également contenir cela sizeof(Node) = k + sizeof(Data) = sizeof(Node) + sizeof(Data)et ensuite sizeof(Node) > sizeof(Node).
bitmask

4
@bitmask aucune taille valide n'existe dans les nombres réels . Si vous autorisez les transinfinis, aleph_0fonctionne. (Juste être trop pédant :-))
k_g

2
@k_g Eh bien, le standard C / C ++ exige que la valeur de retour de sizeofsoit un type intégral non signé, il y a donc l'espoir de tailles transfinies ou même réelles. (être encore plus pédant!: p)
Thomas

@Thomas: On pourrait même dire qu'il y a même les nombres naturels. (Aller au-dessus du -pedantic top: p)
bitmask

1
En fait, Noden'est même pas défini avant la fin de cet extrait de code, vous ne pouvez donc pas vraiment l'utiliser à l'intérieur. Permettre à quelqu'un de déclarer implicitement des pointeurs vers une classe encore non déclarée est une petite astuce qui est autorisée par le langage afin de rendre de telles structures possibles, sans avoir besoin de lancer explicitement des pointeurs tout le temps.
osa

13

Ce dernier ( Node m_next) devrait contenir le nœud. Cela ne le montrerait pas. Et il n'y aurait alors aucune liaison d'éléments.


3
Pire encore, il serait logiquement impossible pour un objet de contenir quelque chose du même type.
Mike Seymour

N'y aurait-il pas encore techniquement de liaison parce que ce serait un nœud contenant un nœud contenant un nœud et ainsi de suite?
m0meni

9
@ AR7: Non, le confinement signifie qu'il est littéralement à l'intérieur de l'objet, pas lié à celui-ci.
Mike Seymour

9

L'approche que vous décrivez est compatible non seulement avec C ++, mais aussi avec son langage sous - ensemble ( la plupart du temps) C . Apprendre à développer une liste chaînée de style C est un bon moyen de vous familiariser avec les techniques de programmation de bas niveau (comme la gestion manuelle de la mémoire), mais ce n'est généralement pas le cas. une bonne pratique pour le développement C ++ moderne.

Ci-dessous, j'ai implémenté quatre variantes sur la façon de gérer une liste d'éléments en C ++.

  1. raw_pointer_demoutilise la même approche que la vôtre - la gestion manuelle de la mémoire requise avec l'utilisation de pointeurs bruts. L'utilisation de C ++ ici est uniquement pour le sucre syntaxique , et l'approche utilisée est par ailleurs compatible avec le langage C.
  2. Dans shared_pointer_demola liste, la gestion se fait toujours manuellement, mais la gestion de la mémoire est automatique (n'utilise pas de pointeurs bruts). Ceci est très similaire à ce que vous avez probablement expérimenté avec Java.
  3. std_list_demoutilise le listconteneur de bibliothèque standard . Cela montre à quel point les choses deviennent plus faciles si vous comptez sur des bibliothèques existantes plutôt que sur la vôtre.
  4. std_vector_demoutilise le vectorconteneur de bibliothèque standard . Cela gère le stockage de la liste dans une seule allocation de mémoire contiguë. En d'autres termes, il n'y a pas de pointeurs vers des éléments individuels. Dans certains cas plutôt extrêmes, cela peut devenir considérablement inefficace. Dans les cas typiques, cependant, il s'agit de la meilleure pratique recommandée pour la gestion des listes en C ++ .

À noter: De tous ces éléments, seul le raw_pointer_demorequiert en fait que la liste soit explicitement détruite afin d'éviter une «fuite» de mémoire. Les trois autres méthodes détruiraient automatiquement la liste et son contenu lorsque le conteneur sort du cadre (à la fin de la fonction). Le fait est que C ++ est capable d'être très "Java" à cet égard - mais seulement si vous choisissez de développer votre programme en utilisant les outils de haut niveau à votre disposition.


/*BINFMTCXX: -Wall -Werror -std=c++11
*/

#include <iostream>
#include <algorithm>
#include <string>
#include <list>
#include <vector>
#include <memory>
using std::cerr;

/** Brief   Create a list, show it, then destroy it */
void raw_pointer_demo()
{
    cerr << "\n" << "raw_pointer_demo()..." << "\n";

    struct Node
    {
        Node(int data, Node *next) : data(data), next(next) {}
        int data;
        Node *next;
    };

    Node * items = 0;
    items = new Node(1,items);
    items = new Node(7,items);
    items = new Node(3,items);
    items = new Node(9,items);

    for (Node *i = items; i != 0; i = i->next)
        cerr << (i==items?"":", ") << i->data;
    cerr << "\n";

    // Erase the entire list
    while (items) {
        Node *temp = items;
        items = items->next;
        delete temp;
    }
}

raw_pointer_demo()...
9, 3, 7, 1

/** Brief   Create a list, show it, then destroy it */
void shared_pointer_demo()
{
    cerr << "\n" << "shared_pointer_demo()..." << "\n";

    struct Node; // Forward declaration of 'Node' required for typedef
    typedef std::shared_ptr<Node> Node_reference;

    struct Node
    {
        Node(int data, std::shared_ptr<Node> next ) : data(data), next(next) {}
        int data;
        Node_reference next;
    };

    Node_reference items = 0;
    items.reset( new Node(1,items) );
    items.reset( new Node(7,items) );
    items.reset( new Node(3,items) );
    items.reset( new Node(9,items) );

    for (Node_reference i = items; i != 0; i = i->next)
        cerr << (i==items?"":", ") << i->data;
    cerr<<"\n";

    // Erase the entire list
    while (items)
        items = items->next;
}

shared_pointer_demo()...
9, 3, 7, 1

/** Brief   Show the contents of a standard container */
template< typename C >
void show(std::string const & msg, C const & container)
{
    cerr << msg;
    bool first = true;
    for ( int i : container )
        cerr << (first?" ":", ") << i, first = false;
    cerr<<"\n";
}

/** Brief  Create a list, manipulate it, then destroy it */
void std_list_demo()
{
    cerr << "\n" << "std_list_demo()..." << "\n";

    // Initial list of integers
    std::list<int> items = { 9, 3, 7, 1 };
    show( "A: ", items );

    // Insert '8' before '3'
    items.insert(std::find( items.begin(), items.end(), 3), 8);
    show("B: ", items);

    // Sort the list
    items.sort();
    show( "C: ", items);

    // Erase '7'
    items.erase(std::find(items.begin(), items.end(), 7));
    show("D: ", items);

    // Erase the entire list
    items.clear();
    show("E: ", items);
}

std_list_demo()...
A:  9, 3, 7, 1
B:  9, 8, 3, 7, 1
C:  1, 3, 7, 8, 9
D:  1, 3, 8, 9
E:

/** brief  Create a list, manipulate it, then destroy it */
void std_vector_demo()
{
    cerr << "\n" << "std_vector_demo()..." << "\n";

    // Initial list of integers
    std::vector<int> items = { 9, 3, 7, 1 };
    show( "A: ", items );

    // Insert '8' before '3'
    items.insert(std::find(items.begin(), items.end(), 3), 8);
    show( "B: ", items );

    // Sort the list
    sort(items.begin(), items.end());
    show("C: ", items);

    // Erase '7'
    items.erase( std::find( items.begin(), items.end(), 7 ) );
    show("D: ", items);

    // Erase the entire list
    items.clear();
    show("E: ", items);
}

std_vector_demo()...
A:  9, 3, 7, 1
B:  9, 8, 3, 7, 1
C:  1, 3, 7, 8, 9
D:  1, 3, 8, 9
E:

int main()
{
    raw_pointer_demo();
    shared_pointer_demo();
    std_list_demo();
    std_vector_demo();
}

La Node_referencedéclaration ci-dessus aborde l'une des différences les plus intéressantes au niveau du langage entre Java et C ++. En Java, déclarer un objet de type Nodeutiliserait implicitement une référence à un objet alloué séparément. En C ++, vous avez le choix entre l'allocation de référence (pointeur) et l'allocation directe (pile) - vous devez donc gérer la distinction explicitement. Dans la plupart des cas, vous utiliseriez l'allocation directe, mais pas pour les éléments de liste.
Brent Bradburn

Je ne sais pas pourquoi je n'ai pas également recommandé la possibilité d'un std :: deque .
Brent Bradburn le

8

Aperçu

Il existe 2 façons de référencer et d'allouer des objets en C ++, alors qu'en Java, il n'y a qu'une seule façon.

Afin d'expliquer cela, les schémas suivants montrent comment les objets sont stockés en mémoire.

1.1 Eléments C ++ sans pointeurs

class AddressClass
{
  public:
    int      Code;
    char[50] Street;
    char[10] Number;
    char[50] POBox;
    char[50] City;
    char[50] State;
    char[50] Country;
};

class CustomerClass
{
  public:
    int          Code;
    char[50]     FirstName;
    char[50]     LastName;
    // "Address" IS NOT A pointer !!!
    AddressClass Address;
};

int main(...)
{
   CustomerClass MyCustomer();
     MyCustomer.Code = 1;
     strcpy(MyCustomer.FirstName, "John");
     strcpy(MyCustomer.LastName, "Doe");
     MyCustomer.Address.Code = 2;
     strcpy(MyCustomer.Address.Street, "Blue River");
     strcpy(MyCustomer.Address.Number, "2231 A");

   return 0;
} // int main (...)

.......................................
..+---------------------------------+..
..|          AddressClass           |..
..+---------------------------------+..
..| [+] int:      Code              |..
..| [+] char[50]: Street            |..
..| [+] char[10]: Number            |..
..| [+] char[50]: POBox             |..
..| [+] char[50]: City              |..
..| [+] char[50]: State             |..
..| [+] char[50]: Country           |..
..+---------------------------------+..
.......................................
..+---------------------------------+..
..|          CustomerClass          |..
..+---------------------------------+..
..| [+] int:      Code              |..
..| [+] char[50]: FirstName         |..
..| [+] char[50]: LastName          |..
..+---------------------------------+..
..| [+] AddressClass: Address       |..
..| +-----------------------------+ |..
..| | [+] int:      Code          | |..
..| | [+] char[50]: Street        | |..
..| | [+] char[10]: Number        | |..
..| | [+] char[50]: POBox         | |..
..| | [+] char[50]: City          | |..
..| | [+] char[50]: State         | |..
..| | [+] char[50]: Country       | |..
..| +-----------------------------+ |..
..+---------------------------------+..
.......................................

Avertissement : la syntaxe C ++ utilisée dans cet exemple est similaire à la syntaxe de Java. Mais, l'allocation de mémoire est différente.

1.2 Eléments C ++ utilisant des pointeurs

class AddressClass
{
  public:
    int      Code;
    char[50] Street;
    char[10] Number;
    char[50] POBox;
    char[50] City;
    char[50] State;
    char[50] Country;
};

class CustomerClass
{
  public:
    int           Code;
    char[50]      FirstName;
    char[50]      LastName;
    // "Address" IS A pointer !!!
    AddressClass* Address;
};

.......................................
..+-----------------------------+......
..|        AddressClass         +<--+..
..+-----------------------------+...|..
..| [+] int:      Code          |...|..
..| [+] char[50]: Street        |...|..
..| [+] char[10]: Number        |...|..
..| [+] char[50]: POBox         |...|..
..| [+] char[50]: City          |...|..
..| [+] char[50]: State         |...|..
..| [+] char[50]: Country       |...|..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|         CustomerClass       |...|..
..+-----------------------------+...|..
..| [+] int:      Code          |...|..
..| [+] char[50]: FirstName     |...|..
..| [+] char[50]: LastName      |...|..
..| [+] AddressClass*: Address  +---+..
..+-----------------------------+......
.......................................

int main(...)
{
   CustomerClass* MyCustomer = new CustomerClass();
     MyCustomer->Code = 1;
     strcpy(MyCustomer->FirstName, "John");
     strcpy(MyCustomer->LastName, "Doe");

     AddressClass* MyCustomer->Address = new AddressClass();
     MyCustomer->Address->Code = 2;
     strcpy(MyCustomer->Address->Street, "Blue River");
     strcpy(MyCustomer->Address->Number, "2231 A");

     free MyCustomer->Address();
     free MyCustomer();

   return 0;
} // int main (...)

Si vous vérifiez la différence entre les deux méthodes, vous verrez que dans la première technique, l'élément d'adresse est alloué au sein du client, tandis que dans la seconde, vous devez créer chaque adresse de manière explicite.

Attention: Java alloue des objets en mémoire comme cette seconde technique, mais la syntaxe est comme la première, ce qui peut prêter à confusion pour les nouveaux venus en "C ++".

la mise en oeuvre

Ainsi, votre exemple de liste pourrait être quelque chose de similaire à l'exemple suivant.

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

.......................................
..+-----------------------------+......
..|            Node             |......
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|            Node             +<--+..
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|            Node             +<--+..
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................v..
...................................[X].
.......................................

Résumé

Étant donné qu'une liste liée a une quantité variable d'éléments, la mémoire est allouée selon les besoins et selon les disponibilités.

METTRE À JOUR:

Il convient également de le mentionner, comme l'a commenté @haccks dans son message.

Cela parfois, des références ou des pointeurs d'objet, indiquent des éléments imbriqués (alias "UML Composition").

Et parfois, des références ou des pointeurs d'objet, indiquent des éléments externes (aka "UML Aggregation").

Mais les éléments imbriqués de la même classe ne peuvent pas être appliqués avec la technique "sans pointeur".


7

Par ailleurs, si le tout premier membre d'une classe ou d'une structure est le pointeur suivant (donc pas de fonctions virtuelles ou toute autre fonctionnalité d'une classe qui signifierait que le suivant n'est pas le premier membre d'une classe ou d'une structure), alors vous peut utiliser une classe ou une structure "de base" avec juste un pointeur suivant, et utiliser du code commun pour les opérations de base de liste chaînée comme ajouter, insérer avant, récupérer à partir de l'avant, .... En effet, C / C ++ garantit que l'adresse du premier membre d'une classe ou d'une structure est la même que l'adresse de la classe ou de la structure. La classe ou structure de nœud de base n'aurait qu'un pointeur suivant à utiliser par les fonctions de liste chaînée de base, puis le transtypage serait utilisé selon les besoins pour convertir entre le type de nœud de base et les types de nœud «dérivés». Note latérale - en C ++, si la classe de nœud de base n'a qu'un pointeur suivant,


6

Pourquoi est-il préférable d'utiliser des pointeurs dans une liste chaînée?

La raison en est que lorsque vous créez un Nodeobjet, le compilateur doit allouer de la mémoire pour cet objet et pour cela, la taille de l'objet est calculée.
La taille du pointeur vers n'importe quel type est connue du compilateur et, par conséquent, avec le pointeur auto-référentiel, la taille de l'objet peut être calculée.

Si Node m_nodeest utilisé à la place, le compilateur n'a aucune idée de la taille de Nodeet il restera bloqué dans une récursivité infinie du calcul sizeof(Node). Rappelez-vous toujours: une classe ne peut pas contenir un membre de son propre type .


5

Parce que cela en C ++

int main (..)
{
    MyClass myObject;

    // or

    MyClass * myObjectPointer = new MyClass();

    ..
}

équivaut à cela en Java

public static void main (..)
{
    MyClass myObjectReference = new MyClass();
}

où les deux créent un nouvel objet MyClassutilisant le constructeur par défaut.


0

Pourquoi les listes liées utilisent-elles des pointeurs au lieu de stocker des nœuds à l'intérieur des nœuds?

Il y a bien sûr une réponse triviale.

Si elles ne sont pas un lien d' un nœud à l'autre par un pointeur, ils ne seraient pas des listes liées .

L'existence des listes chaînées en tant que chose est due au fait que nous voulons pouvoir enchaîner des objets entre eux. Par exemple: nous avons déjà un objet de quelque part. Nous voulons maintenant mettre cet objet réel (pas une copie) à la fin d'une file d'attente, par exemple. Ceci est réalisé en ajoutant un lien du dernier élément déjà sur la file d'attente à l'entrée que nous ajoutons. En termes de machine, c'est remplir un mot avec l'adresse de l'élément suivant.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.