أدوات C++ الحديثة: optional و variant و string_view — C++ المتقدم
أدوات C++ الحديثة
أضافت C++17 أنواعاً صغيرة تحل مشاكل يومية كانت تُحل بحيل مثل "أعد -1 عند الفشل" أو union خطيرة. هذا الدرس يعرض أهمها.
std::optional: قيمة قد لا توجد
كيف تعيد دالة البحث "لم أجد"؟ إرجاع -1 يخلط القيمة بالخطأ (ماذا لو كانت -1 قيمة صالحة؟)، ورمي استثناء مبالغة لحالة متوقعة. optional<T> إما يحمل T أو لا شيء:
#include <iostream>
#include <optional>
#include <string>
#include <vector>
using namespace std;
optional<int> findIndex(const vector<string>& v, const string& target) {
for (size_t i = 0; i < v.size(); i++)
if (v[i] == target) return static_cast<int>(i);
return nullopt; // "لا شيء"
}
int main() {
vector<string> names = {"سارة", "عمر"};
if (auto idx = findIndex(names, "عمر")) // optional يُقيَّم كـ bool
cout << "وُجد في " << *idx << '\n';
cout << findIndex(names, "زيد").value_or(-1) << '\n'; // -1
}| العملية | المعنى |
|---|---|
| if (opt) / opt.has_value() | هل توجد قيمة؟ |
| *opt | القيمة (سلوك غير معرّف إن كانت فارغة) |
| opt.value() | القيمة أو يرمي bad_optional_access |
| opt.value_or(x) | القيمة أو x |
structured bindings: تفكيك بأسماء
#include <map>
#include <string>
#include <tuple>
using namespace std;
tuple<int, int, double> stats() { return {3, 97, 81.5}; }
void demo() {
auto [count, best, avg] = stats(); // ثلاثة أسماء دفعة واحدة
map<string, int> m = {{"أ", 1}};
for (const auto& [key, value] : m) { /* ... */ }
if (auto [it, inserted] = m.insert({"ب", 2}); inserted) { /* أُضيف */ }
}لاحظ if (init; condition) — تعريف متغير يعيش داخل الشرط فقط (C++17).
std::variant: واحد من عدة أنواع
variant<int, string> يحمل إما int أو string، ويعرف أيهما — بديل آمن لـ union:
#include <iostream>
#include <string>
#include <variant>
using namespace std;
int main() {
variant<int, string> v = 42;
cout << get<int>(v) << '\n';
v = string("نص");
cout << boolalpha << holds_alternative<string>(v) << '\n'; // true
if (auto p = get_if<int>(&v)) cout << *p; // لا يُطبع: ليست int الآن
// get<int>(v) هنا يرمي bad_variant_access
}visit: التعامل مع كل الاحتمالات
std::visit تستدعي دالة بالنوع الموجود فعلاً. مع lambda عامة و if constexpr:
#include <type_traits>
#include <variant>
struct Circle { double r; };
struct Rect { double w, h; };
using Shape = std::variant<Circle, Rect>;
double area(const Shape& s) {
return std::visit([](const auto& sh) -> double {
using T = std::decay_t<decltype(sh)>;
if constexpr (std::is_same_v<T, Circle>) return 3.14159 * sh.r * sh.r;
else return sh.w * sh.h;
}, s);
}variant مقابل الوراثة: variant مناسب عندما تكون الأنواع معروفة ومغلقة (لا يضيف أحد أنواعاً جديدة) — بلا مؤشرات ولا heap ولا virtual. الوراثة مناسبة عندما تريد أن يضيف الآخرون أنواعاً جديدة.
std::string_view: نص بلا نسخ
const string& تجبر المستدعي على امتلاك std::string؛ تمرير "نص حرفي" يُنشئ string مؤقتاً. string_view "نافذة" على نص موجود (مؤشر + طول) تقبل أي نوع نص بلا نسخ:
#include <string_view>
std::size_t countSpaces(std::string_view text) {
std::size_t n = 0;
for (char c : text) if (c == ' ') ++n;
return n;
}
// countSpaces("a b c")، countSpaces(myString)، countSpaces(myString.substr(2)) — كلها تعمل⚠️ string_view لا تملك النص. لا تُعِدها من دالة تشير إلى string محلي، ولا تخزنها بعد موت النص الأصلي:
#include <string>
#include <string_view>
std::string_view bad() {
std::string local = "مؤقت";
return local; // ❌ مرجع معلّق: local يموت هنا
}استخدمها للمعاملات للقراءة فقط، وخزّن std::string في الأعضاء.
ملخص
| تحتاج | استخدم |
|---|---|
| قيمة قد تغيب | optional<T> |
| قيمة من مجموعة أنواع ثابتة | variant<A, B, C> + visit |
| إرجاع عدة قيم بأسماء | struct أو tuple + structured bindings |
| معامل نصي للقراءة فقط | string_view |
تمرين
اكتب optional<int> parseAge(string_view text) تعيد العمر إذا كان النص أرقاماً بين 0 و 150 فقط. ثم عرّف using Token = variant<int, string>; وقسّم جملة إلى tokens (أرقام ونصوص) واطبع كل token بنوعه باستخدام visit.
#include <iostream>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <variant>
#include <vector>
using namespace std;
// ===== optional: نتيجة قد تغيب =====
optional<int> parseAge(string_view text) {
if (text.empty() || text.size() > 3) return nullopt;
int value = 0;
for (char c : text) {
if (c < '0' || c > '9') return nullopt;
value = value * 10 + (c - '0');
}
if (value > 150) return nullopt;
return value;
}
// ===== variant: قيمة من أنواع محددة =====
struct Circle { double r; };
struct Rect { double w, h; };
struct Triangle { double base, height; };
using Shape = variant<Circle, Rect, Triangle>;
double area(const Shape& s) {
return visit([](const auto& sh) -> double {
using T = decay_t<decltype(sh)>;
if constexpr (is_same_v<T, Circle>) return 3.14159 * sh.r * sh.r;
else if constexpr (is_same_v<T, Rect>) return sh.w * sh.h;
else return 0.5 * sh.base * sh.height;
}, s);
}
string shapeName(const Shape& s) {
switch (s.index()) { // رقم النوع الحالي حسب ترتيبه في variant
case 0: return "دائرة";
case 1: return "مستطيل";
default: return "مثلث";
}
}
// ===== string_view: قراءة نص بلا نسخ =====
size_t countWords(string_view text) {
size_t words = 0;
bool inWord = false;
for (char c : text) {
if (c == ' ') inWord = false;
else if (!inWord) { inWord = true; ++words; }
}
return words;
}
int main() {
cout << "=== optional ===\n";
for (string_view input : {"25", "abc", "200", "7"}) {
auto age = parseAge(input);
cout << input << " → " << (age ? to_string(*age) : string("غير صالح")) << "\n";
}
cout << "القيمة الافتراضية: " << parseAge("x").value_or(-1) << "\n";
cout << "\n=== variant + visit ===\n";
vector<Shape> shapes = {Circle{1}, Rect{2, 3}, Triangle{4, 5}};
for (const auto& s : shapes) cout << shapeName(s) << ": " << area(s) << "\n";
cout << "\n=== structured bindings ===\n";
map<string, int> scores = {{"سارة", 91}, {"عمر", 78}};
for (const auto& [name, score] : scores) cout << name << " = " << score << "\n";
if (auto [it, inserted] = scores.insert({"ليلى", 85}); inserted)
cout << "أُضيفت " << it->first << "\n";
cout << "\n=== string_view ===\n";
string sentence = "تعلم لغة C++ الحديثة خطوة خطوة";
cout << "عدد الكلمات: " << countWords(sentence) << "\n";
cout << "من نص حرفي: " << countWords("بدون نسخ أي شيء") << "\n";
return 0;
}اختبر فهمك