42 votes

Éviter la croissance exponentielle des références const et des références rvalue dans le constructeur

Je suis codage de certaines basées sur des modèles de classes pour une machine d'apprentissage de la bibliothèque, et je suis confronté à ce problème un grand nombre de fois. J'utilise principalement la politique patron de, où les classes recevoir comme argument de modèle de politiques pour des fonctions différentes, par exemple:

template <class Loss, class Optimizer> class LinearClassifier { ... }

Le problème est avec les constructeurs. Comme le montant des politiques (paramètres du modèle) se développe, les combinaisons de const références et références rvalue croître de façon exponentielle. Dans l'exemple précédent:

LinearClassifier(const Loss& loss, const Optimizer& optimizer) : _loss(loss), _optimizer(optimizer) {}

LinearClassifier(Loss&& loss, const Optimizer& optimizer) : _loss(std::move(loss)), _optimizer(optimizer) {}

LinearClassifier(const Loss& loss, Optimizer&& optimizer) : _loss(loss), _optimizer(std::move(optimizer)) {}

LinearClassifier(Loss&& loss, Optimizer&& optimizer) : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {}

Est-il un moyen pour éviter cela?

36voto

lisyarus Points 9611

En fait, c'est la raison précise pour laquelle le transfert parfait a été introduit. Réécrire le constructeur comme

template <typename L, typename O>
LinearClassifier(L && loss, O && optimizer)
    : _loss(std::forward<L>(loss))
    , _optimizer(std::forward<O>(optimizer))
{}

Mais il sera probablement beaucoup plus simple de faire ce que Ilya Popov suggère dans sa réponse. Pour être honnête, j'ai l'habitude de le faire de cette façon, puisque les mouvements sont destinés à être bon marché et un de plus ne change pas les choses de façon spectaculaire.

Comme Howard Hinnant a dit, ma méthode peut être SFINAE hostile, depuis maintenant LinearClassifier accepte n'importe quelle paire de types dans le constructeur. Barry réponse montre la façon de traiter avec elle.

31voto

Ilya Popov Points 1489

C'est exactement le cas d'utilisation de "passage par valeur et passer à la technique. Bien que légèrement moins efficace que la lvalue/rvalue surcharges, il n'est pas trop mal (un mouvement supplémentaire) et vous permet d'économiser les tracas.

LinearClassifier(Loss loss, Optimizer optimizer) 
    : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {}

Dans le cas de la lvalue argument, il y aura une copie ou un déplacement, dans le cas de rvalue argument, il y aura deux déplacements (à condition que vous les classes Loss et Optimizer mettre en œuvre des constructeurs de déplacement).

Mise à jour: En général, le transfert parfait solution est plus efficace. D'autre part, cette solution évite basées sur des modèles de constructeurs qui ne sont pas toujours souhaitable, car il accepte les arguments de n'importe quel type lorsqu'il n'est pas contraint à SFINAE et conduire à des erreurs matérielles dans le constructeur si les arguments ne sont pas compatibles. En d'autres termes, sans contrainte basée sur des modèles par les constructeurs ne sont pas SFINAE de l'environnement. Voir Barry de répondre à une contrainte du modèle du constructeur qui permet d'éviter ce problème.

Un autre problème potentiel basé sur un modèle du constructeur est la nécessité de le placer dans un fichier d'en-tête.

Mise à jour 2: Herb Sutter parle de ce problème dans son CppCon 2014 parler de "Retour aux fondamentaux", en commençant à 1:03:48. Il parle de passage par valeur d'abord, puis de la surcharge sur les rvalue-ref, puis transfert parfait à 1:15:22 , y compris contraignant. Et enfin, il parle de constructeurs comme le seul bon cas d'utilisation pour le passage par valeur au 1:25:50.

29voto

Barry Points 45207

Par souci d'exhaustivité, la meilleure 2-argument constructeur devrait prendre deux transfert des références et de l'utilisation SFINAE pour s'assurer qu'ils sont les types appropriés. On peut introduire le alias suivantes:

template <class T, class U>
using decays_to = std::is_convertible<std::decay_t<T>*, U*>;

Et puis:

template <class L, class O,
          class = std::enable_if_t<decays_to<L, Loss>::value &&
                                   decays_to<O, Optimizer>::value>>
LinearClassifier(L&& loss, O&& optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{ }

Cela garantit que nous n'acceptons que les arguments de type Loss et Optimizer (ou dérivés). Malheureusement, il est tout à fait une bouchée à écrire et est très distrayant de l'intention d'origine. C'est assez difficile d'obtenir le droit - mais si la performance est importante, alors c'est important, et c'est vraiment la seule façon d'aller.

Mais si elle n'a pas d'importance, et si Loss et Optimizer sont bon marché de se déplacer (ou, mieux encore, le rendement de ce constructeur est complètement hors de propos), préférez Ilya Popov de la solution:

LinearClassifier(Loss loss, Optimizer optimizer)
: _loss(std::move(loss))
, _optimizer(std::move(optimizer))
{ }

16voto

Yakk Points 31636

Dans quelle mesure vers le bas le trou de lapin voulez-vous aller?

Je suis conscient de 4 décent façons d'aborder ce problème. Vous devez utiliser généralement les plus anciennes si vous correspondez à leurs conditions, comme chaque plus tard, on l'augmente de manière significative dans la complexité.


Pour la plupart, soit déplacer est donc pas cher de le faire deux fois, c'est gratuit, ou de déplacer la copie.

Si un déménagement est la copie, et la copie est non-libre, prenez le paramètre par const&. Si pas, le prendre par la valeur.

Cela se comportent exactement de manière optimale, et de votre code beaucoup plus facile à comprendre.

LinearClassifier(Loss loss, Optimizer const& optimizer)
  : _loss(std::move(loss))
  , _optimizer(optimizer)
{}

pour un bon marché à déplacer Loss et déplacez-est-copie optimizer.

Ce n'1 extra déplacer sur la "optimale" de transfert parfait ci-dessous (remarque: le transfert parfait n'est pas optimale) par la valeur du paramètre dans tous les cas. Tant que déplacer n'est pas cher, c'est la meilleure solution, car il génère des propres messages d'erreur, permet aux {} en fonction de la construction, et est beaucoup plus facile à lire que les autres solutions.

Envisager l'utilisation de cette solution.


Si un déménagement est moins cher que la copie mais non-libre, une approche de transfert parfait en fonction: Soit:

template<class L, class O    >
LinearClassifier(L&& loss, O&& optimizer)
  : _loss(std::forward<L>(loss))
  , _optimizer(std::forward<O>(optimizer))
{}

Ou la plus complexe et plus la surcharge de l'environnement:

template<class L, class O,
  std::enable_if_t<
    std::is_same<std::decay_t<L>, Loss>{}
    && std::is_same<std::decay_t<O>, Optimizer>{}
  , int> * = nullptr
>
LinearClassifier(L&& loss, O&& optimizer)
  : _loss(std::forward<L>(loss))
  , _optimizer(std::forward<O>(optimizer))
{}

cela vous coûte la capacité à faire, {} en fonction de la construction de vos arguments. Aussi, jusqu'à nombre exponentiel de constructeurs peut être généré par le code ci-dessus s'ils sont appelés (j'espère qu'ils seront inline).

Vous pouvez déposer l' std::enable_if_t clause au prix de SFINAE échec; en gros, le mal de surcharge de votre constructeur peut être choisi si vous n'êtes pas prudent avec ce std::enable_if_t de la clause. Si vous avez des surcharges de constructeur avec le même nombre d'arguments, ou de se soucier de début de l'échec, alors vous voulez l' std::enable_if_t on. Sinon, utilisez le plus simple.

Cette solution est généralement considéré comme "optimal". Il est accepably optimal, mais il n'est pas le plus optimal.


La prochaine étape consiste à utiliser emplace construction avec des n-uplets.

private:
template<std::size_t...LIs, std::size_t...OIs, class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
  std::index_sequence<LIs...>, std::tuple<Ls...>&& ls,
  std::index_sequence<OIs...>, std::tuple<Os...>&& os
)
  : _loss(std::get<LIs>(std::move(ls))...)
  , _optimizer(std::get<OIs>(std::move(os))...)
{}
public:
template<class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
  std::tuple<Ls...> ls,
  std::tuple<Os...> os
):
  LinearClassifier(std::piecewise_construct_t{},
    std::index_sequence_for<Ls...>{}, std::move(ls),
    std::index_sequence_for<Os...>{}, std::move(os)
  )
{}

où nous reporter la construction jusqu'à l'intérieur de l' LinearClassifier. Cela vous permet d'avoir non-copier/objets mobiles dans votre objet, et est sans doute une efficacité maximale.

Pour voir comment cela fonctionne, exemple maintenant, piecewise_construct travaille avec std::pair. Vous passez par morceaux construisons d'abord, puis forward_as_tuple les arguments pour construire chaque élément de la suite (y compris la copie ou le déplacement ctor).

En construisant directement des objets, nous pouvons éliminer un déménagement ou une copie par objet par rapport à la parfaite solution de transfert ci-dessus. Il vous permet également de transférer une copie ou un déplacement si nécessaire.


Une finale mignon technique est de type effacement de la construction. En pratique, il faut quelque chose comme std::experimental::optional<T> à être disponible, et pourrait faire de la classe un peu plus grand.

C'est pas plus rapide que le cas de la construction d'un. Il n'résumé le travail que l'emplace la construction d'un, ce qui rend plus simple sur une base unitaire, et il vous permet de diviser ctor corps à partir du fichier d'en-tête. Mais il y a une petite quantité de surcharge, à la fois de l'exécution et de l'espace.

Il ya un tas de standard vous avez besoin pour commencer avec. Cela génère une classe de modèle qui représente le concept de "construction de l'objet, plus tard, à l'endroit de quelqu'un d'autre va me dire."

struct delayed_emplace_t {};
template<class T>
struct delayed_construct {
  std::function< void(std::experimental::optional<T>&) > ctor;
  delayed_construct(delayed_construct const&)=delete; // class is single-use
  delayed_construct(delayed_construct &&)=default;
  delayed_construct():
    ctor([](auto&op){op.emplace();})
  {}
  template<class T, class...Ts,
    std::enable_if_t<
      sizeof...(Ts)!=0
      || !std::is_same<std::decay_t<T>, delayed_construct>{}
    ,int>* = nullptr
  >
  delayed_construct(T&&t, Ts&&...ts):
    delayed_construct( delayed_emplace_t{}, std::forward<T>(t), std::forward<Ts>(ts)... )
  {}
  template<class T, class...Ts>
  delayed_construct(delayed_emplace_t, T&&t, Ts&&...ts):
    ctor([tup = std::forward_as_tuple(std::forward<T>(t), std::forward<Ts>(ts)...)]( auto& op ) mutable {
      ctor_helper(op, std::make_index_sequence<sizeof...(Ts)+1>{}, std::move(tup));
    })
  template<std::size_t...Is, class...Ts>
  static void ctor_helper(std::experimental::optional<T>& op, std::index_sequence<Is...>, std::tuple<Ts...>&& tup) {
    op.emplace( std::get<Is>(std::move(tup))... );
  }
  void operator()(std::experimental::optional<T>& target) {
    ctor(target);
    ctor = {};
  }
  explicit operator bool() const { return !!ctor; }
};

où nous type d'effacement à l'action de la construction d'une option à partir des arguments arbitraires.

LinearClassifier( delayed_construct<Loss> loss, delayed_construct<Optimizer> optimizer ) {
  loss(_loss);
  optimizer(_optimizer);
}

_loss sont std::experimental::optional<Loss>. Pour supprimer le caractère facultatif de l' _loss vous devez utiliser std::aligned_storage_t<sizeof(Loss), alignof(Loss)> et d'être très prudent sur l'écriture d'un ctor pour gérer les exceptions manuellement et de détruire les choses etc. C'est un mal de tête.

Quelques belles choses à propos de ce dernier modèle est que le corps de la ctor peut sortir de la tête, et à plus d'un linéaire de la quantité de code est généré à la place d'une exponentielle de la quantité de modèle de constructeurs.

Cette solution est légèrement moins efficace que le placement de construire version, comme tous les compilateurs seront en mesure de inline l' std::function d'utilisation. Mais elle permet aussi de stocker des non-objets mobiliers.

Code non testé, donc il y a probablement des fautes de frappe.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X