L'autre réponse est la suivante :
Ce n'est qu'une théorie, mais j'imagine que la raison pour laquelle il n'est pas limité à const est de permettre aux implémenteurs de muter leur implémentation si nécessaire. De plus, cela permet de conserver une interface plus uniforme - si certains operator() sont const et d'autres non-const, cela devient un peu fouillis.
C'est en grande partie correct, mais c'est encore plus profond que cela dans le contexte de la programmation générique. (Comme l'a dit @Calimo, cela laisse l'idée que const
a été omise "au cas où").
Après avoir réfléchi, je suis arrivé à la conclusion que le problème se traduit par la question de savoir si la fonction membre suivante peut être en principe const
dépend en fait du type de _UniformRandomNumberGenerator
.
template<typename _UniformRandomNumberGenerator>
result_type
operator()(_UniformRandomNumberGenerator& __urng)
À ce niveau de spécification (générique), cela n'est pas connu, et c'est donc seulement à ce moment-là que "[la spécification] permet aux implémenteurs de muter [l'état interne]" et elle le fait pour des raisons de généricité.
Le problème de la constance est donc que au moment de la compilation il faut savoir si _UniformRandomNumberGenerator
est capable de générer suffisamment d'aléas (bits) pour que la distribution produise un échantillon.
Dans la spécification actuelle, cette possibilité est laissée de côté, mais elle peut en principe être mise en œuvre (ou spécifiée) en ayant deux versions exclusives de la fonction membre :
template<typename _URG, typename = std::enable_if<not has_enough_randomness_for<_URG, result_type>::value > >
result_type
operator()(_UniformRandomNumberGenerator& __urng){..statefull impl..}
template<typename _URG, typename = std::enable_if<has_enough_randomness_for<_URG, result_type>::value > >
result_type
operator()(_UniformRandomNumberGenerator& __urng) const{..stateless impl...}
Dónde has_enough_randomness_for
est une métafonction booléenne imaginée qui permet de savoir si l'implémentation particulière peut être sans état.
Cependant, il existe encore un autre obstacle : en général, la question de savoir si la mise en œuvre est sans état ou non dépend de l'état de l'application. paramètres d'exécution de la distribution. Mais comme il s'agit d'une information d'exécution, elle ne peut pas être transmise en tant que partie du système de types !
Comme vous le voyez, cela ouvre une autre boîte de Pandore. constexpr
Les paramètres des distributions pourraient en principe détecter cela, mais je comprendrais tout à fait que la commission s'arrête là.
Si vous avez besoin d'une distribution immuable (par exemple pour être "conceptuellement" correcte), vous pouvez facilement l'obtenir en payant un prix :
- Copier une distribution vierge à chaque fois avant de l'utiliser.
- Mettre en œuvre soi-même la logique de distribution d'une manière sans état.
(1) peut être très inefficace et (2) il est probable qu'il soit quelque peu inefficace et (3) il est probable qu'il soit quelque peu inefficace. extrêmement difficile à mettre en œuvre correctement.
Étant donné que (2) est presque impossible à réaliser en général et que même si l'on y parvient, ce sera quelque peu inefficace, je vais seulement montrer comment mettre en œuvre une distribution sans état qui fonctionne :
template<class Distribution>
struct immutable : Distribution{
using Distribution::Distribution;
using Distribution::result_type;
template<typename _URG> result_type operator()(_URG& __urng) const{
auto dist_copy = static_cast<Distribution>(*this);
return dist_copy(__urng);
}
// template<typename _URG> result_type operator()(_URG& __urng) = delete;
};
De manière à ce que immutable<D>
remplace D
. (Autre nom pour immutable<D>
pourrait être conceptual<D>
.)
J'ai testé ceci avec uniform_real_distribution
par exemple, et le immutable
le remplacement est presque deux fois plus lent (parce qu'il copie/modifie/abandonne un état nominal), mais comme vous le soulignez, il peut être utilisé dans un contexte plus "conceptuel" si c'est important pour votre conception (ce que je peux comprendre).
(Il y a un autre avantage mineur non lié qui est que vous pouvez utiliser une distribution immuable partagée entre les threads).
LE CODE SUIVANT EST INCORRECT MAIS ILLUSTRATIF :
Pour illustrer la difficulté de l'exercice (2), je vais faire un naïf spécialisation de immutable<std::uniform_int_distribution>
qui est presque correcte pour certaines utilisations (ou très incorrecte selon la personne à qui l'on s'adresse).
template<class Int>
struct immutable<std::uniform_int_distribution<Int>> : std::uniform_int_distribution<Int>{
using std::uniform_int_distribution<Int>::uniform_int_distribution;
using std::uniform_int_distribution<Int>::result_type;
template<typename _URG> result_type operator()(_URG& __urng) const{
return __urng()%(this->b() - this->a()) + this->a(); // never do this ;) for serious stuff, it is wrong in general for very subtle reasons
}
// template<typename _URG> result_type operator()(_URG& __urng) = delete;
};
Cette implémentation sans état est très "efficace" mais n'est pas 100% correcte pour des valeurs arbitraires de a
et b
(limites de la distribution). Comme vous pouvez le constater, pour d'autres distributions (y compris les distributions continues), cette voie est très difficile, délicate et sujette à erreur, et je ne la recommande donc pas.
Il s'agit principalement d'une opinion personnelle : Les situations peuvent-elles être améliorées ?
Oui, mais seulement un peu.
Les distributions pourraient avoir deux versions de operator()
, un no- const
(c'est-à-dire &
), qui est optimale (actuelle) et une qui est const
qu'il fait ce qu'il peut pour ne pas modifier l'état. Toutefois, il n'est pas certain qu'ils soient cohérents d'un point de vue déterministe (c'est-à-dire qu'ils donnent les mêmes réponses). (Même le recours à la copie ne donnera pas les mêmes résultats qu'une distribution mutable à part entière). Cependant, je ne pense pas que ce soit une voie viable (je suis d'accord avec l'autre réponse) ; soit vous utilisez une version immuable, soit une version immuable, mais pas les deux en même temps.
Ce que je pense pouvoir faire, c'est d'avoir une version mutable mais une surcharge spécifique pour les références aux valeurs r ( operator() &&
). De cette manière, le mécanisme de la version mutable peut être utilisé, mais l'étape désormais "inutile" de la mise à jour (par exemple, la réinitialisation) de l'état peut être omise parce que l'instance particulière ne sera plus jamais utilisée. On peut ainsi économiser certaines opérations dans certains cas.
De cette manière, le immutable
décrit ci-dessus peut être écrit de cette manière et exploiter la sémantique :
template<class Distribution>
struct immutable : Distribution{
using Distribution::Distribution;
using Distribution::result_type;
template<typename _URG> result_type operator()(_URG& __urng) const{
auto dist_copy = static_cast<Distribution>(*this);
return std::move(dist_copy)(__urng);
// or return (Distribution(*this))(__urng);
}
};
0 votes
Les opérateurs () dans les distributions sont non-const en standard... Utilisez donc reference au lieu de const-reference.
5 votes
Oui, je comprends que c'est défini dans la norme C++, mais je n'en comprends pas la raison. Par exemple, la distribution uniforme int peut être entièrement paramétrée par ses bornes gauche et droite, la distribution normale par la moyenne et l'écart type, la distribution discrète par les probabilités individuelles, etc. Cela peut donc être fait au moment de la construction, et il semble qu'il n'y ait pas de raison de permettre de changer l'instance de la distribution (en particulier pour
operator()
).0 votes
Je ne connais pas le fonctionnement de l'opérateur () pour les distributions, mais il se peut que l'une d'entre elles change d'état dans cette fonction ? La distribution est une interface et doit satisfaire aux exigences du tableau 118 (25.1.6/3).
11 votes
Les distributions sont (conceptuellement) des fonctions avec état (en pratique, certaines peuvent être mises en œuvre sans état).
4 votes
@R.MartinhoFernandes Je dirais que c'est le cas. La distribution est un concept mathématique, et la valeur qui sera générée par la distribution suivante ne dépend pas de la valeur qui a été générée au moment précédent. Si quelqu'un doit garder un état caché pour éviter un calcul ou pour une autre raison, il devrait utiliser le spécificateur mutable pour les champs, mais logiquement, la distribution doit être immuable.
0 votes
Supposons qu'il s'agisse
const
. Supposons ensuite que vous l'ayez transmis à un certain nombre de fonctions dans une rangée utilisantoperator()
- ils produiraient alors (probablement, en raison de la mise en œuvre) tous les mêmes nombres parce qu'aucun état n'a été modifié. C'est une mauvaise chose.0 votes
@karlicoss c'est peut-être vrai pour les vrais générateurs de nombres aléatoires, mais ce n'est absolument pas comme cela que fonctionne la quasi-totalité des PRNG.
3 votes
@Yuushi non, l'état est modifié et c'est l'état caché du générateur que nous passons à la fonction
operator()
4 votes
@karlicoss Je pensais que vous utilisiez le C++ et que vous ne faisiez pas de mathématiques. En C++, les distributions sont des fonctions avec état.
2 votes
@R.MartinhoFernandes oui, j'utilise c++, mais lorsque nous utilisons des concepts mathématiques dans des langages de programmation, nous essayons d'écrire le code de manière à ce qu'il corresponde à la théorie si cela n'entraîne pas de complications excessives, n'est-ce pas ? Donc je comprends qu'en c++ ils sont stateful, je ne comprends pas pourquoi ils le sont.
0 votes
@karlicoss Désolé, je vous ai mal compris. J'ai posté une ébauche de réponse maintenant que j'ai compris ce que vous demandiez (bonne question d'ailleurs), mais je ne peux certainement pas donner une réponse définitive. J'espère que quelqu'un de mieux informé pourra vous éclairer sur ce point.
0 votes
@Yuushi pas de problème :)
2 votes
Tout l'état du générateur aléatoire devrait se trouver dans le moteur, et non dans la distribution. Je ne pense pas que cela ait un sens pour
operator()
de ne pas avoir de qualificatif const.