少写代码就是好代码

一种理想的语言,针对一个问题应该提供尽可能少的实现方式,这样,即使用户只掌握了这门语言的大概,也可避免误用。但是反过来说,这种语言是不存在的。Python在代码格式上算是做到了这一点,但是以严格的缩进代替明确的首尾标记,自动格式化也就不太可能了,这种缩进一旦丢失也很难恢复。很多语言刚发明时,都力求简洁,没有太多花哨的语法和功能,但是一路走来,复杂的语法和功能越加越多,做一件事就不止一种两种甚至三种方式了。哪一种情况下用哪一种方式,就得看程序员的素质。

我常常看到项目中,有像下面这样的代码:

bool bVisible = (FLAG_SHOW == strValue) ? true : false;

其实,这算是好的情况了,如果你跟他说这个代码可以写得更简单些:

bool bVisible = (FLAG_SHOW == strValue);

他可能会跟你争辩,说加上true/false的写法更容易理解。这就难说了,因为是否容易理解是个仁者见仁智者见智的问题,说不定还有审美趣味的因素,而我真的没有更好的理由说服他,毕竟,这只是个微不足道的单行代码而已。

下面的if...else语句与此类似,每次看到它我都要强忍住随手改之的冲动。

if (<bool表达式>) {
  return true;
} else {
  return false;
}

少写代码意味着不写不必要的代码。避免坏代码最好的方法就是少写代码。少写代码往往比多写代码需要更多的经验和觉悟。比如让你对一组浮点数进行排序:

void sort(float* values, size_t size);

最简单的方法就是使用STL的sort():

void sort(float* values, size_t size) {
  std::sort(values, values + size);
}

这样的代码再好不过了。前提是你首先得了解STL(经验),其次要能抑制住重新发明轮子的冲动(觉悟)。

看一个我们项目中的例子,下面这个函数替换子字符串:

std::string replace(const std::string & src,const std::string & oldstr,const std::string & newstr)
{
  size_t pos = 0;
  std::string buffer = src;
  std::string::size_type newlen = newstr.length();
  std::string::size_type oldlen = oldstr.length();
  while(true)
  {
    pos = buffer.find(oldstr, pos);
    if (pos == std::string::npos) break;
    buffer.replace(pos, oldlen, newstr);
    pos += newlen;
  }
  return(buffer);
}

我之所注意到这个函数,是因为最近一位同事在重构时对它进行了优化,重构前它是下面这个样子:

std::string str::replace(const std::string & src,const std::string & oldstr,const std::string & newstr)
{
  std::string buffer=src;
  const std::string::size_type oldlen=oldstr.length();
  if (oldlen>0)
  {
    std::string::size_type index=0;
    std::string::size_type newlen=newstr.length();
    if (newlen)
    {
      if (newlen<oldlen)
      {
        if ((index=newstr.find(oldstr))!= std::string::npos)
        {
          newlen=index;
        }
      }
    }
    while ((index=buffer.find(oldstr,index))!= std::string::npos)
    {
      buffer.replace(index,oldlen,newstr);
      index+=newlen;
    }
  }
  return(buffer);
}

我想说的是,这个重构做得很不到位。其实对于字符串替换这种问题,肯定有人早就做过了,并且做得很好,比如boost algorithm就提供了replace_all/replace_all_copy。所以,这个函数可以直接删掉,用boost好了。

在我们项目中,重新发明轮子的事情实在是太多了,比如下面这个从字符串转浮点数的函数(应该是从网上拷贝来的):

static double string_to_double(const char* s)
{
    double val = 0,power = 1;
    int i,sign = 1;
    for(i=0;isspace(s[i]);i++);
    sign=(s[i]=='-')?-1:1;
    if(s[i]=='+'||s[i]=='-')
        i++;
    for(val=0.0;isdigit(s[i]);i++)
        val=10.0*val+(s[i]-'0');
    if(s[i]=='.')
        i++;
    for(power=1.0;isdigit(s[i]);i++)
    {
        val=10.0*val+(s[i]-'0');
        power*=10.0;
    }
    return sign*val/power;
}

当然,还有double_to_string,我都不好意思贴出来了。

少写代码不是说可以随便拷贝别人的代码,特别是从网上。再举一个我们项目中的一个例子(靠!你有没有发现我们项目的伟大!?),读取一个文本文件,把内容存在字符串里:

bool ReadFile(const std::string& file, std::string& stream)
{
    /*
     * C version -- read file
     */
    // Read file characters line by line(perfect)
    FILE* fpin = NULL;
    const int   maxLen = 180; // Assume the character number of a line is less than 180
    char  buf[maxLen];
    fpin = fopen(file.c_str(), "r");
    if (NULL == fpin) return false;
    while (!feof(fpin) && fgets (buf , maxLen , fpin)) // Judge the end of file
    {
        stream += buf;
    }
    fclose(fpin);
    return true;
}

这段代码是从网上拷的,连注释都保留原样。"假定每行字符数少于180"这一句特别显眼,这种假设是荒谬的。

没有评论
2012年1月10日 | 归档于 C/C++
标签:

Vim中避免缩进C++ Namespace

Vim的cinoptions不支持定制C++的namespace缩进。不过,可以重写indent文件,把下面这个文件放到vimfiles/indent目录,即可。
cpp.vim如下:

" Only load this indent file when no other was loaded.
if exists("b:did_indent")
  finish
endif
let b:did_indent = 1
" Set the function to do the work.
setlocal indentexpr=GetCppIndent()
" Only define the function once.
if exists("*GetCppIndent")
  finish
endif
function GetCppIndent()
  let indent = cindent(v:lnum)
  if v:lnum < 2
    return indent
  endif
  " Don't indent namespace block.
  let prev = v:lnum - 1
  let pline = getline(prev)
  if pline =~ '^\s*namespace\s\+\a\+\s*{\s*$'
    let indent = indent - &sw
  elseif pline =~ '^\s*{\s*$'
    if getline(prev - 1) =~ '^\s*namespace\s\+\a\+\s*$'
      let indent = indent - &sw
    endif
  endif
  return indent
endfunction

用VC的同学就比较悲催了:http://blog.csdn.net/ganxinjiang/archive/2010/09/13/5880541.aspx

没有评论
2011年12月31日 | 归档于 未分类
标签:

在Vim里给utf-8编码的文件加上BOM

BOM: Byte Order Mark
简单来说,设置bomb选项(bomb/nobomb)。

今天编辑cue文件,中文曲目在foobar里不能正常显示,google后发现foobar是支持utf-8编码的cue文件的,只是要加上BOM。

于是在Vim里:h bom,得到方法如上。

没有评论
2011年12月27日 | 归档于 未分类
标签:

C++函数字符串参数的优化

C++的字符串类一般都可以从C字符串隐式地(implicitly)构造,比如std::string之于const char*。所以,以字符串对象为参数的函数也可以接受C字符串,等等。缺点是会产生临时字符串对象。

void findByName(const std:string& name) {
  // ...
}
findByName("adam"); // 这里会产生一个临时std::string对象。

有时候,这种临时对象是不可避免的,假如findByName在内部只接受和处理std::string,这时,即使参数上是const char*,到了里面还是要生成std::string对象。但是很多时候,这种临时对象都是可以避免的,std::string做得就很好,比如成员函数compare,它既接受const std::string&也接受const char*,find等成员函数也是如此。

假如你想写一个函数,把字符串转换成bool:

// "true"/"false" (case ignored) -> true/false
// "1"/"0" -> true/false
... BoolFromString(...)

首先提供参数为const char*的实现:

bool BoolFromString(const char* input, bool* output) {
    if (_stricmp(input, "true") == 0 || strcmp(input, "1") == 0) {
        *output = true;
        return true;
    } else if (_stricmp(input, "false") == 0 || strcmp(input, "0") == 0) {
        *output = false;
        return true;
    }
    return false;
}

然后,参数为const std::string&的实现只是简单的转调:

bool BoolFromString(const std::string& input, bool* output) {
    return BoolFromString(input.c_str(), output);
}

这里很容易犯的错误是把转调的关系搞反:

bool BoolFromString(const char* input, bool* output) {
    return BoolFromString(std::string(input), output);
}

这样的实现没有任何意义,干脆不要提供const char*的实现。

这种字符串参数的优化看似微不足道,但不可小觑,对于频繁处理字符串的程序(比如编译器),必然是可观的性能改进。

这种改进实施起来对程序员的素质要求比较高,于是就有了辅助的手段:Goolge Chromium提供了StringPiece类,而LLVM则提供了StringRef类。

StringPiece是一个指向一片固定大小内存的类字符串对象(注释原文:A string-like object that points to a sized piece of memory),类的定义大致如下:

class StringPiece {
 public:
  typedef size_t size_type;
 private:
  const char*   ptr_;
  size_type     length_;
 public:
  StringPiece() : ptr_(NULL), length_(0) { }
  StringPiece(const char* str)
    : ptr_(str), length_((str == NULL) ? 0 : strlen(str)) { }
  StringPiece(const std::string& str)
    : ptr_(str.data()), length_(str.size()) { }
  StringPiece(const char* offset, size_type len)
    : ptr_(offset), length_(len) { }
  const char* data() const { return ptr_; }
  size_type size() const { return length_; }
  size_type length() const { return length_; }
  bool empty() const { return length_ == 0; }

StringPiece可由const char*,std::string或者一块非'\0'结尾的字符数组构造,但它不会拷贝内存。这就是它的特点。它所指向的内存必须要有较其自身更长的生命期,这便决定了它比较适合作为函数参数,而不是作为成员变量或者放在容器中。

使用StringPiece来实现BoolFromString,不必有两个函数,一个就够了:

bool BoolFromString(const StringPiece& input, bool* output) {
    if (_stricmp(input.data(), "true") == 0 || strcmp(input.data(), "1") == 0) {
        *output = true;
        return true;
    } else if (_stricmp(input.data(), "false") == 0 || strcmp(input.data(), "0") == 0) {
        *output = false;
        return true;
    }
    return false;
}

这个BoolFromString可以接受const char*,std::string,甚至StringPiece,不管哪一种,都没有关于字符串的多余的内存分配和释放。

LLVM的StringRef,实现上跟Google的StringPiece一样,接口方面略有差别。作为参数时,Google建议StringPiece用const&,而LLVM则直接传值。StringRef也可以传const&,但是它的名字起得不好,已经带有引用的含义了(Ref),效率上当然传const&更高。所以我更喜欢Google的StringPiece。

没有评论
2011年11月28日 | 归档于 C/C++
标签:

启发式思维之倒推法两例


倒推法之所以是一种极为深刻的思维方法,本质上是因为它充分利用了题目中一个最不易被察觉到的信息——结论。

刘未鹏《暗时间》132页提及的两个例子。

例一:用9升的桶和4升的桶各一个从河里取6升的水。

思维过程:6 = 9 - 3,9已知,3未知,3 = 4 - 1,4已知,1未知,1 = 9 - 4 - 4,9和4皆已知。

所以解法是:用9升的桶取9升水,往4升的桶里倒两次,余1升水,存在4升的桶中;再用9升的桶取9升水,把已经有1升水的4升的桶倒满,则9升的桶中便剩6升水。

当然, 反向推导时,也有可能走偏。比如我一开始就这样思考的:6 = 5 +1,5 = 9 - 4,1 = 5 - 4,这一过程中5 + 1难以实现,因为9升的桶和4升的桶分别只有一个,如果有多个,还是可行的。

例二:100根火柴,两个人轮流取,每人每次只能取1~7根,谁拿到最后一根谁赢;问有必胜策略吗?有的话,是先手还是后手必胜?

设两个人为A和B,假定B必胜,先考虑最后一步(第N步), 每一步都是A先取,B后取。反向推导过程如下:

第N步:必须为8根,这样A随便怎么取(1~7根),B都可以一次将剩余的取完。

第N-1步:要想第N步有8根,B取之前必须为9~15根,那么A取之前必须为16根。

第N-2步:要想第N-1步有16根,B取之前必须为17~23根,那么A取之前必须为24根。

第N-3步:要想第N-2步有24根,B取之前必须为25~31根,那么A取之前必须为32根。

......

这时,注意到数字8,16,24,32,......都是8的倍数。也就是说,只要B取完后,保证剩下的火柴数为8的倍数就可以了。考虑100根火柴的情况,B想必胜的话,最好B先取4根,剩下96根(8 * 12),然后A取,B再取,保证剩下88根,依此类推。最终B胜。

简单来说:假定火柴总数为N根,B先取N%8(N模8)根,随后A取m根,B就取8-m根。

如果A不知道此必胜方法,那么即使A先取,B也可能获胜。只要在当中的某步,保证B取后剩余8 * N根即可。

至此,我们没有考虑N为8的倍数的情况,当总数为8的倍数时,就得让A先取了。

3 条评论
2011年11月23日 | 归档于 思维方法
标签: