C++ 的模闆是 C++ 的一個重要的語言特性,我們使用的 STL 就是 Standard Template Library 的縮寫,但是在很多情況下,開發者都對其敬而遠之,有些團隊甚至是直接在工程中禁用模闆。模闆常被當作洪水猛獸的一個原因是許多人提起模闆就要提 C++ 模闆圖靈完備,甚至還要再秀一段編譯期排序,這種表現模闆強大的方式不僅不會讓人覺得模闆有用,反而讓人覺得模闆難以了解而且不應該使用。在這篇文章裡,我将聊一下最近實際工程中的一些模闆的應用,希望可以讓更多人了解到模闆并不是一個可怕的存在,以及一些常見的使用方式。
按版本号過濾配置
我所在的項目組前背景的複雜配置現在都用 protobuf 進行承載,然後生成 Excel 進行配置,生成 C++ 代碼進行加載。例如這樣的 message:
message ConfigItem1 {
int32 id = 1;
string text = 2;
}
message Config {
repeated ConfigItem1 items1 = 1;
}
複制
這裡的
Config
會被映射為一個 Excel,裡面有一個表
items1
,其中,這個表有兩列,一列
id
,一列
text
。這個表的每一行都是一個具體的配置項。也就是我們可以這樣擷取配置:
cout << cfg.items1(0).id() << ": " << cfg.items1(0).text();
複制
現在有個需求是這樣的,在加載某些配置的時候,前台需要根據版本号進行配置的過濾,部配置設定置項隻會在某些版本中可見,例如這樣:
message VersionRange {
int32 lo = 1;
int32 hi = 2;
}
message ConfigItem2 {
repeated VersionRange version_ranges = 1;
int32 id = 2;
int32 value = 3;
}
message Config {
repeated ConfigItem1 items1 = 1;
repeated ConfigItem2 items2 = 2;
}
複制
加載的時候大概有這樣的代碼:
// 加載配置時進行過濾
for (auto iter = cfg.items2().begin(); iter != cfg.items2().end();) {
if (!IsAvailableVersion(*iter, ver)) {
iter = cfg.mutable_items2()->erase(iter);
} else {
iter++;
}
}
複制
這個
IsAvailableVersion
要怎麼實作呢?我們當然可以對每個配置項類型都實作一個函數重載,但是我們也可以使用函數模闆來生成這些代碼,非常簡單:
template<class CfgItem>
bool IsAvailableVersion(CfgItem const &item, int ver) {
auto const &ranges = item.version_ranges();
if (std::begin(ranges) == std::end(ranges)) {
return true; // 如果 version range 清單為空,預設傳回 true
}
for (auto const& range : ranges) {
if (ver >= range.lo() && ver <= range.hi()) {
return true; // 如果目前版本在範圍内,就傳回 true
}
}
return false; // 如果 version range 清單不為空,預設傳回 false
}
複制
但這裡有個問題,不是每一個配置項的類型裡都有
version_range
字段,例如
ConfigItem1
就沒有。這就導緻了
IsAvailableVersion
不能對所有的配置項對象進行使用,這不利于我們統一 code gen 上面加載配置時進行過濾的代碼。
這時候,我們想要做的,是對
IsAvailableVersion
的類型參數進行限制,根據這個類型是否帶有
version_range
字段來決定是否進行過濾:
template<class CfgWithVerRange>
bool IsAvailableVersion(CfgWithVerRange const &item, int ver) { /* 實作同上 */ }
template<class CfgWithoutVerRange>
bool IsAvailableVersion(CfgWithoutVerRange const &item, int ver) { return true; }
複制
可惜編譯器沒法通過類型參數的名字明白我們的意圖,是以我們需要用一些技巧達到我們的目的:
template<class CfgItem, class = void>
struct IsAvailableVersionHelper {
static bool Check(CfgItem const&, int) { return true; }
};
template<class CfgItem>
struct IsAvailableVersionHelper<
CfgItem,
lib::void_t<
decltype(std::begin(std::declval<CfgItem>().version_ranges())->lo()),
decltype(std::begin(std::declval<CfgItem>().version_ranges())->hi())
>
> { /* 實作同上 */ };
template<class CfgItem>
bool IsAvailableVersion(CfgItem const &item, int ver) {
return IsAvailableVersionHelper<CfgItem>::Check(item, ver);
}
複制
這時候我們就可以放心地寫
IsAvailableVersion(*iter, ver)
了,如果傳入的是
ConfigItem1
,則使用的是上面原始的實作,而
ConfigItem2
則使用的是下面特化的實作。
這是如何做到的呢?我們知道,C++ 的模闆有個規則是 SFINAE,這不是一個單詞,而是 Substitution Failure Is Not An Error 的縮寫,也就是說,編譯器在基于模闆生成代碼時,如果将模闆的類型參數置換為給定的類型時,如果失敗,編譯器不會報錯,而是将這個結果從可選的集合裡丢棄,并從剩下的中進行選擇。
當我們将
ConfigItem1
放入時,上面的版本能夠正确替換,而下面的版本則因為
ConfigItem1
沒有
version_range
字段而失敗,此時,編譯器會将這個失敗的版本抛棄,由于隻剩下原始版本了,是以選擇了原始版本。
這裡的
lib::void_t
是什麼?
std::void_t
是 C++ 17 之後才在 STL 中提供的模闆,它很簡單也非常有用,功能是将任意的類型序列映射到
void
上,也就是忽略掉這些類型。由于我們在使用 C++ 11,是以需要自己實作一下:
// C++11 中這樣簡單實作可能會有 bug,參考 en.cppreference.com/w/cpp/types/void_t
// template<class...>
// using void_t = void;
template<class... Ts> struct make_void { using type = void; };
template<class... Ts> using void_t = typename make_void<Ts...>::type;
複制
這裡使用
void_t
将多個類型聲明忽略掉以适應
template<class CfgItem, class = void>
中的第二個類型參數:
decltype(std::begin(std::declval<CfgItem>().version_ranges())->lo()),
decltype(std::begin(std::declval<CfgItem>().version_ranges())->hi())
複制
雖然說這兩個類型聲明被忽略了,但是它們還是會參與替換,
decltype
可以根據括号裡的表達式計算出其類型,而
std::declval<T>()
則相反,給定一個類型,它可以獲得該類型的值,雖然這個值并不是有效的,但是在這個類型聲明裡我們可以用它來填寫表達式。如果一個類型沒有帶
version_ranges
字段,則
std::declval<CfgItem>().version_ranges()
會失敗,如果這個
version_range()
傳回的對象不支援
std::begin
,則
std::begin(...)
會失敗,若這個
std::begin
計算出來的疊代器不支援
lo
函數,則
std::begin(...)->lo()
會失敗,這裡的結合就確定了
CfgItem
類型必須有
version_range
,且每一個
version_range
都是可疊代的,且每一個 range 都有
lo
成員。下面的
hi
也是類似的。當然,我們還可以通過
std::is_same
之類的 type trait 進一步確定
lo
和
hi
傳回的類型,這個就不在此示範了,對于我們的需求而言,這樣就足夠了。
那說回來,如果我們填入的是
ConfigItem2
會怎樣?在這個時候,兩個類型替換都會成功,但由于原始版本中,第二個類型參數是預設值
void
,而特化版本中,則填入了自定義的一個類型
lib::void_t...
,雖然這個類型最後計算出來的類型還是
void
,但它依然是比原始版本更「特殊」的版本,是以編譯器會選擇這個版本,這就達到了我們的目的。
Data blob 操作輔助類
在公司中,我們有自己的 NoSQL 資料庫服務,我們在使用的過程中常常有這樣的模式:
MyDataBlob data{};
data.key1 = ...;
data.key2 = ...;
DbApi api(...);
int const res = api.Get("tablename-x", &data, sizeof(data));
if (res == RSP_ERROR && api.GetDbErr() == NOT_EXIST) {
LOGDBG(...); // 資料不存在,列印調試日志
} else if (res != 0) {
LOGERR(...); // 其他錯誤,列印錯誤日志,傳回錯誤
return ERR_DB_GET_FAIL;
}
// 正常邏輯,使用 data ...
複制
這裡先建立一個空白的資料對象,填入它的 key 值,然後調用 API 拉取資料。由于 DB 會将拉取不存在的資料這種情況也認為是一個錯誤,而資料不存在對于業務而言又往往不是一個錯誤,是以我們一般是要對這種情況單獨進行處理。
這種重複的工作顯然可以抽象一個更加友善的 API 類型出來,希望能更輕松地進行使用。一個簡單的想法是這樣的:
template<class Db>
struct Result {
int code{};
int subCode{};
Db data{};
bool IsError() { return code != 0 && subCode != NOT_EXIST; }
bool NoExist() { return code != 0 && subCode == NOT_EXIST; }
}
template<class Db>
struct NewDbApi {
Result Get() {
DbApi api(...);
Result res{};
res.data.SetKey(???); // 1
res.code = api.Get(res.data.TableName(), &res.data, sizeof(res.data)); // 2
res.subCode = api.GetDbErr();
if (res.IsError()) {
LOGERR(...);
}
return res;
}
}
複制
這裡我們碰到了一點麻煩的問題,首先,在 1 處,這個
data.SetKey()
我們不知道應該怎麼填。當然,我們可以像原先一樣在外部自行設定 key,然後再将
data
傳進來,但是我們更加希望能夠免去這一個步驟,直接通過
Get
函數的參數傳入對應的 key,然後轉交給
data
。但我們又不知道這個
Db
類型的 key 是什麼,那我們該怎麼辦呢?也許我們可以這樣做:
template<class ... Args>
Result Get(Args &&...args) {
...
res.data.SetKey(std::forward<Args>(args)...); // 1
...
}
複制
呃……這确實可以實作我們要的效果,但是這個實作方法并不好,它帶來了不必要的複雜度。最讓人難受的一點是,我們丢失了
data.SetKey
所需參數的類型資訊,這讓調用者完全不知道這裡應該填什麼資料。為了解決這個問題,我們可以添加一層抽象,讓
Db
類型告訴我們 key 的類型是什麼:
Result Get(typename Db::key_type const &key) {
res.data.SetKey(key); // 1
...
}
複制
這樣簡單多了,
Get
函數的調用者可以獲知對應的 key 的類型。
另外一個問題是,1 和 2 處我們直接調用了
data
的
SetKey
和
TableName
成員函數,但是我們的
MyDataBlob
是一個用另外一個工具基于 XML 描述生成出來的代碼,主要實作的是序列化和反序列化功能,我們沒法去通過修改這個工具來添加新的接口。是以我們隻能使用 adapter 模式解決這個問題:
struct MyDataBlobAdapter {
using key_type = ...;
void SetKey(key_type const &) { ... }
std::string TableName() const { ... }
MyDataBlob myDataBlob{};
}
複制
這就可以解決上面提到的問題了,給
Get
函數的實作提供了
SetKey
和
TableName
。我們可以發現,
Result
裡的資料類型不再是
MyDataBlob
了,而是
MyDataBlobAdapter
,使用者拿到了這個對象後,使用的方法不再是
res.data
而是
res.data.myDataBlob
了。這在大多數情況下不是什麼大問題,但是,如果一個資料不隻是被這一個接口操作,而是被多個接口操作那該怎麼辦呢?這時候,為了适配多個接口,我們可能需要多個 adapter。例如:
NewDbApi<MyDataBlobAdapter> api1{};
OtherAPI<OtherAdapter> api2{};
Result res = api1.Get(...);
OtherAdapter other(res.data.myDataBlob);
api2.Modify(&other);
res.data.myDataBlob = other.myDataBlob;
api1.Put(res.data);
複制
這不僅麻煩,而且會造成比較大的開銷,在這裡,為了适配兩個接口,我們不得不進行兩次資料的複制。我們能否做得更好呢?
首先注意到
TableName
這個函數其實和對象無關,我們可以實作為一個靜态的函數:
struct MyDataBlobAdapter {
static std::string TableName() const { ... }
...
}
複制
是以上面 2 處的代碼可以改為:
res.code = api.Get(Db::TableName(), ...); // 2
複制
類似地,對于
SetKey
,我們也可以進行類似的改造,雖然它需要操作自己的成員變量,但是,我們可以将
this
指針手動傳遞一下,也就是這樣:
struct MyDataBlobAdapter {
static void SetKey(key_type const &key, MyDataBlobAdapter *adapter) { ... }
...
}
複制
對于使用者,1 處的代碼可以改為:
Db::SetKey(key, &res.data);
複制
這個時候,我們可以發現,這裡
SetKey
的第二個參數根本不需要是
MyDataBlobAdapter*
,我們可以直接将其換為
MyDataBlob*
!同時,類似于
key_type
,為了能告訴使用者這個資料的類型,我們加一個
type
類型聲明,結果是這樣:
struct MyDataBlobAdapter {
using type = MyDataBlob;
static void SetKey(key_type const &key, type *blob) { ... }
...
}
複制
這時我們重新回來看一下
NewDbApi
的實作:
template<class Db>
struct Result {
typename Db::type data{};
...
}
template<class Db>
struct NewDbApi {
Result Get(typename Db::key_type const &k, typename Db::type *db) {
...
Db::SetKey(k, db); // 1
res.code = api.Get(Db::TableName(), &res.data, sizeof(res.data)); // 2
...
}
}
複制
使用的時候,隻需要這樣寫:
NewDbApi<MyDataBlobAdapter> api1{};
OtherAPI<OtherAdapter> api2{};
Result res = api1.Get(...);
api2.Modify(&res.data);
api1.Put(res.data);
複制
這樣一來,我們就實作了既能直接操作資料,又避免給原始類型添加新接口,而且還做到了足夠的泛化靈活,甚至不需要用到虛函數導緻性能損失。
不過,這種形式的實作有個小缺點,這裡的
Db
類型的限制非常不明确,對于使用者而言,可能會碰到非常難讀的編譯錯誤,這可能是許多人害怕模闆的另一個原因。到 C++ 20,我們才能用上 Concept,能夠直接指名模闆參數的限制,但現實情況是,我們可能将長期被鎖在 C++ 11 裡,在這種情況下,我們也可以盡力去給使用者清晰的提示:
// 示例:
// struct LegalDb {
// struct type;
// struct key_type;
// static void SetKey(key_type const &key, type *db);
// static std::string TableName();
// }
template<class Db>
struct NewDbApi {
...
static_assert(IsLegalDb<Db>::value,
"Db must match requirements of LegalDb, see comments above");
}
複制
這樣一來,一旦使用者填入了不合法的類型,編譯期立刻就能收到上面的提示,并且可以基于示例來了解應該如何實作這個類型。這個
IsLegalDb
的實作也用到了 SFINAE,大緻可以實作為這樣:
template<class T, class = void>
struct IsLegalDb: std::false_type {}; // 3
template<class T>
struct IsLegalDb<T,
lib::void_t<
typename T::type,
typename T::key_type,
decltype(T::SetKey(std::declval<typename T::key_type const &>(),
std::declval<typename T::type *>())),
typename std::enable_if<
std::is_convertible<decltype(T::TableName()), std::string>::value>
>::type // 4
>: std::true_type {}; // 5
複制
這裡也用到了前面實作的
void_t
,總體思路是類似的,也是基于類型聲明來讓編譯器選擇我們想要的模闆實作,這裡可能和上一個例子不太一樣的有兩點。第一是我們這裡的類型在 3 和 5 處繼承了
std::true_type
和
std::false_type
,這兩個類型可以認為是類型級别的
true
和
false
,在頭檔案
<type_traits>
裡有很多
is_
開頭的模闆就是基于這兩個類的,如果一個類型符合它的限制,它就是
true_type
否則就是
false_type
。這裡用到的
std::is_convertible
就是這樣的 type trait,它判定的是第一個類型參數能被轉換為第二個類型參數。我們可以用
value
成員的來獲得它們對應的
bool
值。這裡用到了另一個基礎工具是
std::enable_if
,它可以接受一個編譯期計算出來的
bool
值,如果這個值為
true
,那麼我們就能獲得其
type
成員類型,否則就擷取不到,可能直接用一個簡單實作來說明更加友善:
template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { using type = T; };
複制
是以說,4 處的代碼的實作了如果
std::is_convertible
判定為
true
,那麼
std::enable_if
裡就會有
type
,那麼模闆的類型置換就會成功,否則則是失敗,這就實作了我們想要的判定
T::TableName()
傳回類型可以轉換為
std::string
的效果。
IsLegalDb
的實作相對而言可能會有點麻煩,但是它可以帶來清晰的錯誤提示,是一個很好的文檔,是以對于一個有特定限制的模闆類型參數,尤其是無法從名字上直接看出來限制内容的模闆類型參數,最好配套加上這樣一個檢查,配合注釋說明,給使用者明确的限制,以友善使用者實作合法的類型。
強類型别名
我們經常會碰到一個函數帶有幾個類型相同的參數的情況。以撲克牌舉例,一種表示方式是基于花色和數字的表示,使用一個
uint8_t
表示花色,同時一個
uint8_t
表示數字,另一種是直接基于牌編碼的方式,也就是将牌從 0 編号到 54,隻需要一個
uint8_t
就能實作。那麼,如果不同地方使用到了不同的表示方式,就需要有類似這樣的轉換函數:
uint8_t ConvertCardToCode(uint8_t shape, uint8_t number);
複制
這個函數本身是沒什麼問題,但在使用的時候經常一不小心就寫歪了:
auto const num = uint8_t(13);
auto const shp = uint8_t(2);
auto const code = ConvertCardToCode(num, shp); // num 和 shp 的位置寫反了
複制
我們可以通過類型别名聲明來使得函數類型更加明晰:
using CardCode = uint8_t;
using Shape = uint8_t;
using Number = uint8_t;
CardCode ConvertCardToCode(Shape shape, Number number);
複制
這個寫法看起來很不錯,但是在函數調用處,我們仍然無法避免出現這種情況:
auto const num = Number(13);
auto const shp = Shape(2);
auto const code = ConvertCardToCode(num, shp); // 仍然能正常編譯
複制
雖然我們聲明了類型别名,但是這個類型别名的本質上還是原來的類型,我們仍然無法避免出現前面的錯誤。在 Go 語言中,「type alias」(
type T = xxx
)和「type definition」(
type T xxx
)是兩種不同的文法,如果我們使用前者,則依然會遇到上面說的這個問題,但如果我們使用後者,則可以讓編譯器幫我們避免它:
type CardCode uint8;
type Shape uint8;
type Number uint8;
func ConvertCardToCode(shape Shape, number Number) CardCode { /*...*/ }
複制
num := Number(13)
shp := Shape(2)
code := ConvertCardToCode(num, shp); // 編譯失敗
複制
這樣的強類型别名非常好,使得函數簽名本來就成為了注釋的一部分,想要在 C++ 中實作類似的效果,我們可以不是用
using
起别名而是直接将類型包裹一層:
struct Shape {
Shape() = default;
explicit Shape(uint8_t val): v{val} {}
uint8_t v;
};
struct Number {
Number() = default;
explicit Number(uint8_t val): v{val} {}
uint8_t v{};
};
using CardCode = uint8_t;
CardCode ConvertCardToCode(Shape shape, Number number);
複制
此時,函數調用者如果傳錯了參數,就完全沒法編譯通過了:
auto const num = Number(13);
auto const shp = Shape(2);
auto const code = ConvertCardToCode(num, shp); // 編譯出錯
複制
可以發現這兩個類型是很類似的,我們會考慮用模闆來使得這個過程更加便利:
template<class T>
struct StrongAlias {
StrongAlias() = default;
explicit StrongAlias(T val): v{std::move(val)} {}
T v{};
};
using Shape = StrongAlias<uint8_t>;
using Number = StrongAlias<uint8_t>;
using CardCode = uint8_t;
CardCode ConvertCardToCode(Shape shape, Number number);
複制
但很可惜的是,這樣并不能達到我們想要的效果,因為
StrongAlias<uint8_t>
和
StrongAlias<uint8_t>
是同一個類型,是以使用
using
來聲明的
Shape
和
Number
也依然是同一個類型。是以我們需要用另一個标記将兩個類型完全區分開來,我們可以在類型參數清單裡加多一個類型參數來做到這一點,這個類型參數的唯一作用就是用來實作類型的區分:
template<class T, class Tag>
struct StrongAlias {
StrongAlias() = default;
explicit StrongAlias(T val): v{std::move(val)} {}
T v{};
};
using Shape = StrongAlias<uint8_t, struct ShapeTag>;
using Number = StrongAlias<uint8_t, struct NumberTag>;
using CardCode = uint8_t;
CardCode ConvertCardToCode(Shape shape, Number number);
複制
這個實作已經很實用了,但是我們可以讓它更好用一點,目前而言它的不足之處在于,我們包裹的類型往往是一些基礎類型,這些基礎類型自帶了一些操作符,比如我們之前想比較兩張牌是否相等的時候可以寫:
if (card1.shape == card2.shape && card1.number == card2.number) { ... }
複制
但現在需要寫:
if (card1.shape.v == card2.shape.v && card1.number.v == card2.number.v) { ... }
複制
更為麻煩的是,如果我們想将類型别名作為
std::map
的 key 時就會直接報錯:
// using Number = uint8_t;
std::map<Number, int> cardNumCount{}; // 編譯通過
複制
// using Number = StrongAlias<uint8_t, struct NumberTag>;
std::map<Number, int> cardNumCount{}; // 編譯出錯
複制
這是因為
std::map
要求 key 能夠使用
<
進行比較,而當我們直接使用
using
起類型别名時,這個
<
就是
uint8_t
的
<
,而
StrongAlias<uint8_t, struct NumberTag>
則沒有這個運算符。我們當然可以對每一個類型别名都自己實作一次所需的運算符,但我們還可以做得更加簡單:
template<class T, class Tag, template<class> class Op>
struct StrongAliasType: public Op<StrongAliasType<T, Tag, Op>> {
StrongAliasType() = default;
explicit StrongAliasType(T const &value): v(value) {}
T v{};
};
template<class T>
struct Lt {
bool operator<(T const &other) const {
return static_cast<T const &>(*this).v < other.v;
}
};
複制
這裡用到了一個 C++ 裡的一個慣用法——奇異遞歸模闆模式,這個模式裡派生類被作為基類的模闆參數,這個聲明看着有點吓人,但是它實作的效果是很妙的:
using Number = StrongAlias<uint8_t, struct NumberTag, Lt>;
複制
可以看到
StrongAlias<uint8_t, struct NumberTag, Lt>
本身繼承了
Lt<StrongAlias<uint8_t, struct NumberTag, Lt>>
,這意味着
Number
就繼承了
Lt
中的
<
運算符,而
Lt
的
<
實作中,使用了
T::v
的
<
運算符進行比較,是以
Number
就可以使用
uint8_t
的
<
運算符了。
當然,有時候我們可能不止需要這一個運算符,是以
Op
可能不止一個,要想要支援更多運算符,這裡可以使用模闆參數包來實作,使用
...
來辨別一個參數包,然後再用
...
展開:
template<class T, class Tag, template<class> class... Ops>
struct StrongAliasType: public Ops<StrongAliasType<T, Tag, Ops...>>... {
StrongAliasType() = default;
explicit StrongAliasType(T const &value): v(value) {}
T v{};
};
複制
這裡的
StrongAliasType
繼承了類型參數中的每一個
Ops
。然後,類似上面
Lt
的實作,我們可以實作一組這樣的運算符模闆:
template<class T>
struct Eq { bool operator==(T const &other) const { /*...*/ } };
template<class T>
struct Ne { bool operator!=(T const &other) const { /*...*/ } };
template<class T>
struct Lt { bool operator<(T const &other) const { /*...*/ } };
template<class T>
struct Le { bool operator<=(T const &other) const { /*...*/ } };
template<class T>
struct Gt { bool operator>(T const &other) const { /*...*/ } };
template<class T>
struct Ge { bool operator>=(T const &other) const { /*...*/ } };
複制
有了這些運算符模闆,使用者就可以按需選擇自己需要的來進行使用了,例如:
using Number = StrongAlias<uint8_t, struct NumberTag, Eq, Ne, Lt, Le, Gt, Ge>;
複制
這樣,我們就擁有了更加好用的強類型别名了。
小結
在這篇文章裡,我們看到了在實際工程中 C++ 模闆的一些應用。很顯然,這些功能脫離了模闆的能力是非常難以實作的。對于 C++ 開發者而言,不應該盲目地拒絕模闆,而是應該将它應用在正确的地方,以獲得更好的性能和更清晰可靠的代碼。
/* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */ var disqus_shortname = 'ZhiruiLi'; // required: replace example with your forum shortname /* * * DON'T EDIT BELOW THIS LINE * * */ (function() { var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; dsq.src = 'https://' + disqus_shortname + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); })(); /* * * DON'T EDIT BELOW THIS LINE * * */ (function () { var s = document.createElement('script'); s.async = true; s.type = 'text/javascript'; s.src = 'https://' + disqus_shortname + '.disqus.com/count.js'; (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s); }()); comments powered by Disqus