Voici un exemple simplifié. Fondamentalement, il vérifie une chaîne d'une liste de chaînes. Si la vérification réussit, elle supprimera cette chaîne (filterStringOut(i);
), et il n'est plus nécessaire de poursuivre les autres vérifications. Ainsi continue
à la chaîne suivante.
void ParsingTools::filterStrings(QStringList &sl)
{
/* Filter string list */
QString s;
for (int i=0; i<sl.length(); i++) {
s = sl.at(i);
// Improper length, remove
if (s.length() != m_Length) {
filterStringOut(i);
continue; // Once removed, can move on to the next string
}
// Lacks a substring, remove
for (int j=0; j<m_Include.length(); j++) {
if (!s.contains(m_Include.at(j))) {
filterStringOut(i);
/* break; and continue; */
}
}
// Contains a substring, remove
for (int j=0; j<m_Exclude.length(); j++) {
if (s.contains(m_Exclude.at(j))) {
filterStringOut(i);
/* break; and continue; */
}
}
}
}
Comment doit-on continuer la boucle externe depuis l'intérieur d'une boucle imbriquée?
Ma meilleure supposition est d'utiliser goto
et de placer une étiquette à la fin de la boucle externe. Cela m'a incité à poser cette question, étant donné à quel point le tabou goto
peut être.
Dans le chat c ++ IRC, il a été suggéré de placer les boucles for
dans les fonctions booléennes, qui renvoient true si une vérification est réussie.
if ( containsExclude(s)) continue;
if (!containsInclude(s)) continue;
ou que je crée simplement un booléen local, définissez-le sur true break
, vérifiez bool et continuez si true.
Étant donné que j'utilise ceci dans un analyseur, j'ai en fait besoin de hiérarchiser les performances dans cet exemple. Est-ce une situation où goto
est toujours utile, ou est-ce un cas où j'ai besoin de restructurer mon code?
Ne pas imbriquer: convertir en fonctions à la place. Et que ces fonctions renvoient true
si elles effectuent leur action et que les étapes suivantes peuvent être ignorées; false
sinon. De cette façon, vous évitez complètement tout le problème de la façon de sortir d'un niveau, de continuer dans un autre, etc. en enchaînant simplement les appels avec ||
(cela suppose que C++ arrête de traiter une expression sur un true
; je pense que oui).
Ainsi, votre code pourrait finir par ressembler à ce qui suit (je n'ai pas écrit C++ depuis des années, donc il contient probablement des erreurs de syntaxe, mais devrait vous donner une idée générale):
void ParsingTools::filterStrings(QStringList &sl)
{
QString s;
for (int i=0; i<sl.length(); i++) {
s = sl.at(i);
removeIfImproperLength(s, i) ||
removeIfLacksRequiredSubstring(s, i) ||
removeIfContainsInvalidSubstring(s, i);
}
}
bool removeIfImproperLength(QString s, int i) {
if (s.length() != m_Length)
{
filterStringOut(i);
return true;
}
return false;
}
bool removeIfLacksSubstring(QString s, int i) {
for (int j=0; j<m_Include.length(); j++) {
if (!s.contains(m_Include.at(j))) {
filterStringOut(i);
return true;
}
}
return false;
}
bool removeIfContainsInvalidSubstring(QString s, int i) {
for (int j=0; j<m_Exclude.length(); j++) {
if (s.contains(m_Exclude.at(j))) {
filterStringOut(i);
return true;
}
}
return false;
}
D'un point de vue plus d'oiseau, je voudrais refactoriser le code pour qu'il ressemble à ceci ... (en pseudo code, il y a trop longtemps j'ai touché C++)
void filterStrings(sl)
{
/* Filter string list */
for (int i=0; i<sl.length(); i++) {
QString s = sl.at(i);
if(!isProperString(s)) {
filterStringOut(i);
}
}
}
bool isProperString(s) {
if (s.length() != m_Length)
return false; // Improper length
for (int j=0; j<m_Include.length(); j++) {
if (!s.contains(m_Include.at(j))) {
return false; // Lacks a substring
}
}
for (int j=0; j<m_Exclude.length(); j++) {
if (s.contains(m_Exclude.at(j))) {
return false; // Contains a substring
}
}
return true; // all tests passed, it's a proper string
}
C'est IMHO plus propre car il sépare clairement ce qui constitue une chaîne appropriée et ce que vous faites quand ce n'est pas le cas.
Vous pouvez même aller plus loin et utiliser des méthodes de filtrage intégrées comme myProperStrings = allMyStrings.filter(isProperString)
J'aime vraiment comment @dagnelies commence . Court et précis. Une bonne utilisation de l'abstraction de haut niveau. Je ne fais que peaufiner sa signature et éviter un négatif inutile.
void ParsingTools::filterStrings(QStringList &sl)
{
for (int i=0; i<sl.length(); i++) {
QString s = sl.at(i);
if ( isRejectString(s) ) {
filterStringOut(i);
}
}
}
Cependant, j'aime comment @DavidArno décompose les tests d'exigence en tant que fonctions individuelles. Bien sûr, le tout devient plus long mais chaque fonction est merveilleusement petite. Leurs noms évitent d'avoir à commenter ce qu'ils sont. Je n'aime tout simplement pas qu'ils prennent la responsabilité supplémentaire d'appeler filterStringOut()
.
Soit dit en passant, oui C++ arrêtera l'évaluation d'une chaîne ||
Sur un true
tant que vous n'avez pas surchargé l'opérateur ||
. Cela s'appelle évaluation de court-circuit . Mais il s'agit d'une micro-optimisation triviale que vous êtes libre d'ignorer lorsque vous lisez le code tant que les fonctions sont sans effet secondaire (comme celles ci-dessous).
Les éléments suivants devraient rendre la définition d'une chaîne de rejet claire sans vous faire glisser à travers des détails inutiles:
bool isRejectString(QString s) {
return isDifferentLength(s, m_Length)
|| sansRequiredSubstring(s, m_Include)
|| hasForbiddenSubstring(s, m_Exclude)
;
}
Soulagé de la nécessité d'appeler filterStringOut()
les fonctions de test d'exigence deviennent plus courtes et leurs noms sont beaucoup plus simples. J'ai également mis tout ce dont ils dépendent dans leur liste de paramètres pour faciliter leur compréhension sans regarder à l'intérieur.
bool isDifferentLength(QString s, int length) {
return ( s.length() != length );
}
bool sansRequiredSubstring(QString s, QStringList &include) {
for (int j=0; j<include.length(); j++) {
QString requiredSubstring = include.at(j);
if ( !s.contains(requiredSubstring) ) {
return true;
}
}
return false;
}
bool hasForbiddenSubstring(QString s, QStringList &exclude) {
for (int j=0; j<exclude.length(); j++) {
QString forbiddenSubstring = exclude.at(j);
if ( s.contains(forbiddenSubstring) ) {
return true;
}
}
return false;
}
J'ai ajouté requiredSubstring
et forbiddenSubstring
pour les humains. Vont-ils vous ralentir? Testez et découvrez. Il est plus facile de rendre le code lisible réellement rapide que de rendre le code prématurément optimisé lisible ou réellement rapide.
Si les fonctions s'avèrent ralentir, regardez fonctions en ligne avant de soumettre les humains à un code illisible. Encore une fois, ne présumez pas que cela vous donnera de la vitesse. Tester.
Je pense que vous trouverez l'un d'eux plus lisible que imbriqué pour les boucles. Ceux-ci, combinés avec les if
, commençaient à vous donner un vrai flèche anti-motif . Je pense que la leçon ici est que les petites fonctions sont une bonne chose.
Utilisez simplement un lambda pour le prédicat, puis utilisez la puissance de algorithmes standard et de court-circuit. Pas besoin de contrôle-flux alambiqué ou exotique:
void ParsingTools::filterStrings (QStringList& list)
{
for (int i = list.size(); i--;) {
const auto& s = list[i];
auto contains = [&](const QString& x) { return s.contains(x); };
if (s.size() != m_Length
|| !std::all_of(m_Include.begin(), m_Include.end(), contains)
|| std::any_of(m_Exclude.begin(), m_Exclude.end(), contains))
filterStringOut(i);
}
}
Il y a aussi la possibilité de faire le contenu de la boucle externe (celle que vous voulez continuer) a lambda, et utilisez simplement return
.
C'est étonnamment facile si vous connaissez des lambdas; vous commencez fondamentalement votre boucle intérieure avec [&]{
et vous la terminez avec }()
; à l'intérieur, vous pouvez utiliser return;
à tout moment pour le quitter:
void ParsingTools::filterStrings(QStringList &sl)
{
/* Filter string list */
QString s;
for (int i=0; i<sl.length(); i++) {
[&]{ // start a lamdba defintion
s = sl.at(i);
// Improper length, remove
if (s.length() != m_Length) {
filterStringOut(i);
// continue; // Once removed, can move on to the next string
return; // happily return here, this will continue
}
// Lacks a substring, remove
for (int j=0; j<m_Include.length(); j++) {
if (!s.contains(m_Include.at(j))) {
filterStringOut(i);
/* break; and continue; */ return; // happily return here, this will continue the i-loop
}
}
// Contains a substring, remove
for (int j=0; j<m_Exclude.length(); j++) {
if (s.contains(m_Exclude.at(j))) {
filterStringOut(i);
/* break; and continue; */ return; // happily return here, this will continue the i-loop
}
}
}() // close/end the lambda definition and call it
}
}
Je pense que @dganelies a la bonne idée comme point de départ, mais je pense que j'envisagerais d'aller plus loin: écrire une fonction générique qui peut exécuter le même modèle pour (presque) n'importe quel conteneur, critère et action:
template <class Container, class Action, class Condition>
void map_if(Container &container, Action action, Condition cond) {
for (std::size_t i = 0; i < container.length(); i++) {
auto s = container.at(i);
if (cond(s))
action(i);
}
}
Votre filterStrings
définirait alors simplement les critères et passerait l'action appropriée:
void ParsingTools::filterStrings(QStringList const &sl)
{
auto isBad = [&](QString const &s) {
if (s.length() != m_Length)
return true;
for (int j = 0; j < m_Include.length(); j++) {
if (!s.contains(m_Include.at(j))) {
return true;
}
}
for (int j = 0; j < m_Exclude.length(); j++) {
if (s.contains(m_Exclude.at(j))) {
return true;
}
}
return false;
};
map_if(sl, filterStringOut, isBad);
}
Bien sûr, il existe également d'autres façons d'aborder ce problème fondamental. Par exemple, en utilisant la bibliothèque standard, vous semblez vouloir quelque chose dans le même ordre général que std::remove_if
.
Plusieurs réponses suggèrent un refactor majeur du code. Ce n'est probablement pas une mauvaise façon de procéder, mais je voulais apporter une réponse plus conforme à la question elle-même.
Règle n ° 1: Profil avant optimisation
Profilez toujours les résultats avant de tenter une optimisation. Si vous ne le faites pas, vous risquez de perdre beaucoup de temps.
Cela étant dit ...
En l'état, j'ai personnellement testé ce type de code sur MSVC. Les booléens sont la voie à suivre. Nommez quelque chose de booléen sémantiquement significatif comme containsString
.
...
boo containsString = true; // true until proven false
// Lacks a substring, remove
for (int j=0; j<m_Include.length(); j++) {
if (!s.contains(m_Include.at(j))) {
filterStringOut(i);
/* break; and continue; */
containsString = false;
}
}
if (!containsString)
continue;
Sur MSVC (2008), en mode de libération (paramètres d'optimisation typiques), le compilateur optimisait une boucle similaire jusqu'à exactement le même ensemble d'opcodes que la version goto
. Il était assez intelligent pour voir que la valeur du booléen était directement liée au contrôle du flux, et élidait tout. Je n'ai pas testé gcc, mais je suppose qu'il peut faire des types d'optimisation similaires.
Cela a l'avantage par rapport à goto
de simplement ne pas soulever d'inquiétude par les puristes qui considèrent goto
comme nuisible, sans sacrifier la valeur d'une seule instruction.