Commentaire de type pointeur Great Swift

introduction

Swift a ʻUnsafePointer `et ses compagnons comme types pour représenter les pointeurs. Il sera utilisé lors de l'utilisation d'une bibliothèque en langage C telle que Core Foundation. Ces API de type pointeur sont très bien pensées et merveilleuses. Cet article va l'introduire et l'expliquer. Je pense que c'est intéressant pour les utilisateurs du langage C et les utilisateurs C ++. (Swift 3.0.2)

Type de pointeur

Les types de pointeurs sont les suivants.

Type de pointeur de base

Pour le pont de langue

Il y en a tellement.

Attributs de type de pointeur de base

Le type de pointeur le plus basique est ʻUnsafePointer . Représente un pointeur vers une valeur de type T. La valeur T référencée est immuable. ʻUnsafePointer <T> ʻ lui-même est une structure, donc letetvar` peuvent représenter l'immuabilité du pointeur lui-même.

func f(_ x: UnsafePointer<Int>) {
	let a: UnsafePointer<Int> = x
	var b: UnsafePointer<Int> = x
}

Ce qui précède est exprimé en langage C comme suit.

void f(const int * const x) {
	const int * const a = x;
	const int * b = x;
}

Pour ʻUnsafePointer `, il devient un type différent en ajoutant trois attributs. Il existe 8 combinaisons de 2x2x2.

Mutability

Il existe une version mutable dans laquelle la valeur de la référence T peut être modifiée. Mutable est attaché au nom. À partir de la version immuable, il peut être converti avec un constructeur étiqueté «mutating». Inversement, vous pouvez convertir de la version mutable à la version immuable avec le constructeur sans étiquette.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public init(_ other: UnsafeMutablePointer<Pointee>)
    ...
}

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public init(mutating other: UnsafePointer<Pointee>)
    ...
}

Tapé et non tapé

ʻUnsafePointer avait le type T référencé comme paramètre de type, mais certaines versions n'ont pas cette information de type. Les éditions sans informations de type sont nommées avec «Raw». Par exemple, const void * en langage C apparaît à Swift comme ce ʻUnsafeRawPointer. Il est utilisé lorsque le type de la référence du pointeur est inconnu.

À l'origine, ʻUnsafePointer seul pouvait provoquer des bogues d'alias stricts dans certains codes, et ʻUnsafeRawPointer avec la sémantique memcpy était nécessaire pour l'éviter. C'est vrai.

Les détails sont écrits dans le document au moment de la proposition standard. API UnsafeRawPointer

De plus, ce qui suit est facile à comprendre sur l'aliasing strict. (Traduction) Comprendre l'alias strict en C / C ++ ou-pourquoi # $ @ ## @ ^% Le compilateur ne me laisse pas faire ce que je veux!

La conversion entre typé et non typé sera discutée plus tard.

Pointeur et tampon

En langage C, les pointeurs sont souvent utilisés pour représenter des tableaux, mais pour les gérer dans un soi-disant tableau, il est nécessaire de connaître le nombre d'éléments ainsi que les pointeurs. Par conséquent, ʻUnsafeBufferPointer `représente un tableau qui représente ce pointeur et le nombre d'éléments comme un ensemble. Il existe quatre types de "Buffer", correspondant à immuable et mutable, typé et non typé.

ʻUnsafeBufferPointer ʻest conçu pour recevoir l'adresse de départ et le nombre d'éléments dans le constructeur. Et il hérite du protocole Collection.

public struct UnsafeBufferPointer<Element> : Indexable, Collection, RandomAccessCollection {
	...
    public init(start: UnsafePointer<Element>?, count: Int)
    ...
    public var baseAddress: UnsafePointer<Element>? { get }
    ...
    public var count: Int { get }
    ...
}

Moule pour bridge

OpaquePointer

ʻOpaquePointer` est pour représenter le type de motif appelé pointeur opaque en langage C. Un pointeur opaque est un pointeur qui est pré-déclaré uniquement par le nom du type, mais qui n'a pratiquement aucune information sur l'accès à la destination référencée car la définition du type n'est pas visible. Il est également utilisé pour masquer les détails à l'intérieur de la bibliothèque de l'extérieur de la bibliothèque. Dans Swift, il n'arrive pas que seul le nom du type soit visible mais la définition soit invisible, mais ce type est utilisé car il faut l'exprimer lorsque le pointeur opaque défini en langage C est lu depuis Swift. Je vais.

Par exemple, supposons que vous ayez la source C suivante.

struct CatImpl;
struct Cat {
	CatImpl * impl;
}
void PassCat(const Cat * a);
void PassCatImpl(const CatImpl * b);

Depuis Swift, les deux fonctions ressemblent à ceci:

func PassCat(a: UnsafePointer<Cat>?)
func PassCatImpl(b: OpaquePointer?)

ʻUnsafePointer `peuvent être convertis l'un vers l'autre via le constructeur.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public init(_ from: OpaquePointer)
    ...
}

public struct OpaquePointer : Hashable {
	...
    public init<T>(_ from: UnsafePointer<T>)
    ...
}

CVaListPointer

Lorsque nous traitons des arguments de longueur variable en langage C, nous utilisons la notation spéciale ... et le type spécial va_list, mais le pointeur pour traiter va_list est CVaListPointer.

AutoreleasingUnsafeMutablePointer

Non étudié. En regardant un pointeur avec le modificateur __autoreleasing dans Objective-C de Swift, je me sens comme ça, mais je ne comprends pas.

Facultatif, pointeur et nullabilité

Le type de pointeur de Swift ne peut pas être NULL. En d'autres termes, ʻUnsafePointer est un pointeur non nul. Un pointeur Nullable est représenté en utilisant Optional comme ʻUnsafePointer <T>? . ** Swift peut également gérer des pointeurs avec des mécanismes de sécurité nuls tels que la syntaxe ʻif let`. ** **

Il existe une version facultative du constructeur de conversion entre ʻUnsafePointer , ʻUnsafeMutablePointer <T> et ʻOpaquePointer`, et si nil est passé comme argument, le constructeur renvoie également nil.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public init?(_ from: OpaquePointer?)
    ...
    public init?(_ other: UnsafeMutablePointer<Pointee>?)
    ...
}

ʻUnsafeBufferPointer ʻest le type brut et est un pointeur Nullable. Si le pointeur reçu par le constructeur est éventuellement reçu depuis le début et que nil lui est passé, la propriété baseAddress sera nulle.

Accès de base

Les références ʻUnsafePointer sont accessibles avec la propriété pointee. En langage C, il a été écrit avec l'opérateur de déréférence (*) et l'opérateur de flèche (->`). De plus, comme l'accès en indice est possible, la zone de mémoire allouée en continu peut être accédée comme un tableau.

public struct UnsafePointer<Pointee> : Strideable, Hashable {
	...
    public var pointee: Pointee { get }
    ...
    public subscript(i: Int) -> Pointee { get }
    ...
}

Ceci est accessible en écriture dans la version mutable.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public var pointee: Pointee { get nonmutating set }
	...
    public subscript(i: Int) -> Pointee { get nonmutating set }
	...
}

La version Raw non typée ne possède pas ces propriétés. Si vous n'attribuez pas de type, vous ne pourrez pas accéder à la destination de référence.

Trois états du pointeur

Il y a trois états dans la mémoire pointés par ʻUnsafePointer `.

La distinction entre ces trois états est un concept important qui sous-tend le type de pointeur de Swift. Ces trois états sont indiscernables par le système de types. Le programmeur a besoin de savoir exactement dans quel état se trouve le pointeur.

Cependant, ce n'est pas une spécification ajoutée par Swift, mais un concept qui existe essentiellement pour les pointeurs. Ceci est expliqué ci-dessous.

Mémoire sécurisée

L'état alloué signifie que la zone mémoire pointée par le pointeur est sécurisée. Inversement, l'état non alloué signifie que le pointeur est NULL ou que la zone mémoire vers laquelle il pointe est libérée.

Ici, la taille de la zone mémoire gérée par ʻUnsafePointer `n'est pas nécessairement la taille d'une valeur. Il peut également gérer des zones de mémoire allouées en série afin que plusieurs éléments puissent être stockés.

La mémoire peut être allouée par la méthode statique de la version mutante du pointeur, ʻallocate`.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
   	...
    public static func allocate(capacity count: Int) -> UnsafeMutablePointer<Pointee>
    ...
}

L'argument «count» est le nombre de zones mémoire consécutives à allouer. Cette méthode ajuste l'alignement et la foulée en fonction du type de valeur «Pointe». L'alignement est une contrainte sur le rapport de l'adresse mémoire dans laquelle la valeur est placée à la valeur de l'adresse. Par exemple, si l'alignement est 8, l'adresse mémoire sera toujours un multiple de 8. Vous pouvez l'obtenir avec MemoryLayout <T> .alignment. La foulée est la valeur du nombre d'octets que chaque valeur est placée avec l'adresse décalée lors de la sécurisation consécutive. Par exemple, si un type a une taille de mémoire de 5 octets mais une foulée de 8 octets, alors 8 octets sont alloués pour chaque élément, laissant 3 octets libres. Vous pouvez l'obtenir avec MemoryLayout <T> .stride. Ces valeurs sont déterminées par le compilateur et ne peuvent pas être définies arbitrairement par le programmeur.

Pour le ʻUnsafeRawPointer non typé, les paramètres de ʻallocate sont différents car nous ne connaissons pas ces valeurs.

public struct UnsafeMutableRawPointer : Strideable, Hashable {
    public static func allocate(bytes size: Int, alignedTo: Int) -> UnsafeMutableRawPointer
}

Il est conçu pour spécifier le nombre pur d'octets et la valeur d'alignement. Si vous souhaitez réserver en continu, vous devez calculer la taille en tenant compte de la foulée mentionnée ci-dessus.

Les deux valeurs de retour ne sont pas facultatives car le pointeur vers la zone de mémoire allouée est non nul. En outre, ces méthodes statiques sont définies dans la version mutable du type car je ne suis pas satisfait d'allouer une zone de mémoire immuable.

Pour libérer la mémoire allouée, appelez la méthode deallocate.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
    public func deallocate(capacity: Int)
}

État d'initialisation de la mémoire

Initialisé est un concept qui indique si une valeur existe ou non dans la zone mémoire. La zone mémoire qui vient d'être allouée n'est en réalité qu'une zone mémoire, et la valeur n'existe pas encore, elle est dans un état non initialisé.

Qu'il soit initialisé ou non initialisé dépend du moment où vous accédez à la référence du pointeur et ** lisez ** et ** écrivez ** la valeur.

Si vous lisez une valeur dans une zone mémoire non initialisée, vous ne savez pas quel est l'état de la mémoire, donc elle peut contenir des valeurs ridicules et il y a un risque de plantage. Je pense que c'est facile à comprendre. Ce qui est intéressant, c'est quand il s'agit d'écrire.

En général, considérez les variables définies par «var» de Swift. Supposons que vous ayez un type Cat (type de référence) et un type CatHouse (type valeur) qui le contient, comme indiqué ci-dessous.

class Cat {
}

struct CatHouse {
    var cat: Cat?
}

Supposons que le type ʻApp comme indiqué ci-dessous a une propriété du type CatHouse, et ceci est réécrit dans la méthode ʻupdate.

class App {
    init (a: CatHouse) {
        self.a = a
    }

    var a: CatHouse
    
    func update(b: CatHouse) {
        self.a = b
    }
}

A ce moment, le mécanisme ARC de swift augmentera de 1 le compteur de référence de cat que CatHouse ofb a, mais une autre chose à retenir est qu'il est à l'origine dans ʻa. Le processus de décrémentation du compteur de référence cat` que l'ancien CatHouse avait ** se produit. Ainsi, en général, lorsqu'une copie d'une valeur se produit en swift, elle sera effacée par la copie ** l'ancienne valeur sera détruite **.

Maintenant, pensons à écrire une valeur dans la destination de référence du pointeur. Lors de l'écriture d'une valeur dans la destination de référence du pointeur, cela revient à y avoir une variable, il est donc nécessaire de détruire la valeur d'origine. Mais que faire si vous venez de le sécuriser et n'avez pas encore écrit de valeur? Dans cet état, ce serait mauvais si la valeur d'origine était détruite. C'est parce qu'il s'agit d'un état de mémoire aléatoire sans aucune valeur écrite.

Par conséquent, il est nécessaire de faire la distinction entre initialisé et non initialisé. La propriété pointee de ʻUnsafePointer est une convention qui ne doit être utilisée que lorsqu'elle a été initialisée. Utilisez la méthode ʻinitialize pour écrire dans la zone mémoire non initialisée, et la méthode deinitialize pour renvoyer la zone mémoire initialisée à non initialisée.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public func initialize(to newValue: Pointee, count: Int = default)
    ...
    public func deinitialize(count: Int = default) -> UnsafeMutableRawPointer
    ...
    public func move() -> Pointee
    ...
}

Maintenant, comme il était possible d'allouer plusieurs éléments dans la zone mémoire, ces méthodes ont un argument count. Pour ʻinitialize, remplissez les éléments countavec la valeur spécifiée par l'argumentto. À ce stade, la zone de mémoire d'origine n'est pas détruite. Inversement, la méthode deinitializedétruit uniquement la valeur. La méthodemove est deinitializelorsque le nombre d'éléments est égal à un et renvoie la valeur comme valeur de retour. Vous pouvez voir que la sémantique de déplacement est réalisée avec exactement le même nom questd :: move` en C ++.

Je vais expérimenter. Assurez-vous que les ʻinit et deinitdeCat` sont consignés.

class Cat {
    init () {
        print("init")
    }
    deinit {
        print("deinit")
    }
}

Exécutez ensuite la fonction suivante.

func test1() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
    p.move()
}

J'ai essayé de préfixer «deallocate» avec «defer». La sortie sera la suivante.

init
deinit

Maintenant, faisons une version qui ne «bouge» pas.

func test2() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
}

Ensuite, la déinit n'est plus terminée.

init

Bien que la zone de mémoire ait été libérée, le traitement pour réduire le compteur "Cat" qu'elle détenait n'a pas été exécuté car le traitement pour détruire la "CatHouse" qui y était écrite n'a pas été effectué et la mémoire a fui. Je l'ai fait.

Essayez également d'utiliser «pointee» pour effacer les anciennes valeurs.

func test3() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.initialize(to: CatHouse(cat: Cat()))
    p.pointee = CatHouse(cat: Cat())
    p.move()
}
init
init
deinit
deinit

Vous pouvez voir qu'il a été créé deux fois correctement et supprimé deux fois.

Et si vous essayez d'écrire la valeur avant ʻinitialize`?

func test4() {
    var p = UnsafeMutablePointer<CatHouse>.allocate(capacity: 1)
    defer {
        p.deallocate(capacity: 1)
    }
    p.pointee = CatHouse(cat: Cat())
    p.initialize(to: CatHouse(cat: Cat()))
    p.move()
}
init
init
deinit

Comme vous pouvez le voir, un «Cat» a perdu de la mémoire. Le pointee écrit avant ʻinitialize a été écrasé par ** initialisation sans destruction ** pendant ʻinitialize, donc l'opération de compteur de cat a été ignorée et a été divulguée. ..

Et avant cela, ce code ** détruisait la zone non initialisée lors de l'écriture dans pointee ** Il y a donc aussi un risque de crash.

Échange de valeur entre pointeurs et sémantique de déplacement

Supposons que vous ayez deux mémoires allouées, dont l'une a été initialisée. En d'autres termes, supposons qu'il y ait une valeur d'un côté. À ce stade, lors du déplacement de la valeur du pointeur existant vers l'autre pointeur, il existe des modèles 2x2 en fonction des conditions suivantes.

--Si le pointeur de destination est initialisé ou non

La copie de types de valeur est rapide dans Swift, mais si vous avez un type de référence comme propriété, par exemple CatHouse, vous devez incrémenter le compteur de cette référence de 1 lors de la copie, ce qui entraîne la surcharge. Si la valeur de la source de copie est ensuite rejetée, le compteur sera décrémenté de 1 à ce moment-là, de sorte qu'il sera incrémenté de 1 et décrémenté de 1. Par conséquent, s'il y a une opération de rejet de la valeur de la source de copie et en même temps d'affichage de la valeur vers la destination de copie, cette surcharge inutile peut être éliminée. Cela s'appelle une opération de déplacement en C ++, mais le type de pointeur de Swift a une méthode pour ce déplacement.

Comme mentionné précédemment, l'opération d'écriture d'une valeur dans la mémoire non initialisée s'appelait «initialiser». D'autre part, l'opération d'écriture d'une valeur dans la mémoire initialisée est appelée ʻassign. Ces deux sont des copies ordinaires. Et il y a ces versions d'opération de déplacement avec un préfixe appelé move`.

public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
	...
    public func initialize(from source: UnsafePointer<Pointee>, count: Int)
    ...
    public func moveInitialize(from source: UnsafeMutablePointer<Pointee>, count: Int)
    ...
    public func assign(from source: UnsafePointer<Pointee>, count: Int)
    ...
    public func moveAssign(from source: UnsafeMutablePointer<Pointee>, count: Int)
    ...
}

Dans la version de déplacement, source est modifiable. En effet, il est soumis à une opération de rejet.

L'initialisation / la destruction ne peuvent pas être contrôlées pour les systèmes Raw. Parce que le type est inconnu.

État de la mémoire et type de tampon

Le type BufferPointer n'a pas de méthodes telles que l'allocation et l'initialisation. Ces opérations de mémoire sont effectuées par type de pointeur et le tampon agit exactement comme une vue sur celui-ci.

Conversion entre tapé et non typé

La conversion du pointeur typé ʻUnsafePointer en pointeur non typé ʻUnsafeRawPointer est possible dans le constructeur.

public struct UnsafeRawPointer : Strideable, Hashable {
	...
    public init<T>(_ other: UnsafePointer<T>)
    ...
}

Cependant, le pointeur non typé vers la conversion typée n'est pas possible avec le constructeur. Au lieu de cela, il existe deux méthodes dédiées.

public struct UnsafeRawPointer : Strideable, Hashable {
	...
    public func bindMemory<T>(to type: T.Type, capacity count: Int) -> UnsafePointer<T>
    ...
    public func assumingMemoryBound<T>(to: T.Type) -> UnsafePointer<T>
    ...
}

Apparemment, dans le contexte de l'aliasing strict mentionné ci-dessus, ʻUnsafeRawPointer semble avoir le compilateur suivre statiquement avec quel type T` la zone mémoire est actuellement traitée. C'est ce qu'on appelle une liaison.

«Lorsqu'il est alloué en tant que UnsafeRawPointer», il n'est pas lié et la méthode qui le lie à un type «T» est «bindMemory». En même temps, ʻUnsafePointer est renvoyé. Pour la mémoire qui est déjà liée à T, vous pouvez utiliser la méthode ʻassumingMemoryBound.

Il existe également une méthode appelée ʻinitializeMemory` qui initialise la mémoire non initialisée en la tapant dans T.

La transition d'état de liaison ici est décrite dans le document susmentionné. Type de mémoire de liaison

fin

Il fournit les fonctions requises pour les fonctions de langage sans utiliser une syntaxe dédiée pour les pointeurs, gère les informations de type de manière générique, est de sécurité nulle et fournit des méthodes d'opération avec des conventions clairement organisées pour les trois états de la mémoire. , Je pense que c'est très bien fait car il peut également supporter la sémantique de déplacement.

Je pense qu'il est intéressant de le comparer avec Rust et C ++. Dans ces langages, les pointeurs de première classe sont des pointeurs bruts, et les pointeurs intelligents avec des nombres de références, etc. sont fournis sous forme de types génériques. Cependant, dans le cas de Swift, cela est inversé, et le pointeur de première classe est fourni comme un pointeur intelligent et le pointeur brut est fourni comme un type générique. Je pense que ce renversement est un bon équilibre dans la conception d'un langage qui peut être utilisé pour écrire des applications, mais aussi pour les couches basses.

Recommended Posts

Commentaire de type pointeur Great Swift
Conversion du pointeur de chaîne de langage C en type Swift String
[Swift] Type d'énumération de type partagé
[Swift] Résumé du type Bool